From 28cb7d3e2383c972f60b71c2dfc416db84c3340d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:12:24 +0000 Subject: [PATCH 01/68] Bump cookiejar from 2.1.3 to 2.1.4 in /lambda/services Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4. - [Release notes](https://github.com/bmeck/node-cookiejar/releases) - [Commits](https://github.com/bmeck/node-cookiejar/commits) --- updated-dependencies: - dependency-name: cookiejar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- lambda/services/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambda/services/yarn.lock b/lambda/services/yarn.lock index ab3200c5d..5c5898a09 100644 --- a/lambda/services/yarn.lock +++ b/lambda/services/yarn.lock @@ -4842,9 +4842,9 @@ cookie@0.5.0: integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== cookiejar@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" - integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== core-js-compat@^3.25.1: version "3.26.1" From 5f5eb7db0518cb0471d93da58a9ff806a5201a5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 04:24:40 +0000 Subject: [PATCH 02/68] Bump simple-git from 3.15.1 to 3.16.0 in /lambda/services Bumps [simple-git](https://github.com/steveukx/git-js/tree/HEAD/simple-git) from 3.15.1 to 3.16.0. - [Release notes](https://github.com/steveukx/git-js/releases) - [Changelog](https://github.com/steveukx/git-js/blob/main/simple-git/CHANGELOG.md) - [Commits](https://github.com/steveukx/git-js/commits/simple-git@3.16.0/simple-git) --- updated-dependencies: - dependency-name: simple-git dependency-type: indirect ... Signed-off-by: dependabot[bot] --- lambda/services/yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lambda/services/yarn.lock b/lambda/services/yarn.lock index da79ebe4e..f3670daaa 100644 --- a/lambda/services/yarn.lock +++ b/lambda/services/yarn.lock @@ -9675,9 +9675,9 @@ simple-concat@^1.0.0: integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== simple-git@^3.7.0: - version "3.15.1" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.15.1.tgz#57f595682cb0c2475d5056da078a05c8715a25ef" - integrity sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg== + version "3.16.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.16.0.tgz#421773e24680f5716999cc4a1d60127b4b6a9dec" + integrity sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw== dependencies: "@kwsites/file-exists" "^1.1.1" "@kwsites/promise-deferred" "^1.1.1" @@ -10976,4 +10976,4 @@ zip-stream@^4.1.0: dependencies: archiver-utils "^2.1.0" compress-commons "^4.1.0" - readable-stream "^3.6.0" \ No newline at end of file + readable-stream "^3.6.0" From 06f8ef3e7cc54b669dd661d8c77c52db3dedeaea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:50:54 +0000 Subject: [PATCH 03/68] Bump http-cache-semantics from 4.1.0 to 4.1.1 in /web Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/commits) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/yarn.lock b/web/yarn.lock index cb4fc7f60..2c50f3210 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -6122,9 +6122,9 @@ htmlparser2@^6.1.0: entities "^2.0.0" http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-deceiver@^1.2.7: version "1.2.7" From 810e670b6562e842ad24592329b66d21c574f646 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:50:58 +0000 Subject: [PATCH 04/68] Bump http-cache-semantics from 4.1.0 to 4.1.1 in /lambda/services Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/commits) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] --- lambda/services/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambda/services/yarn.lock b/lambda/services/yarn.lock index eced90049..91afe277f 100644 --- a/lambda/services/yarn.lock +++ b/lambda/services/yarn.lock @@ -6425,9 +6425,9 @@ htmlescape@^1.1.0: integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg== http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-errors@2.0.0: version "2.0.0" From 1d583b8ed2f40bf4318d5d37dfde97e1566bb6a8 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Fri, 24 Feb 2023 13:44:08 +0530 Subject: [PATCH 05/68] fixed the decimal value issues in the barcharts --- web/src/Pages/Dashboard/CHART_OPTIONS.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/src/Pages/Dashboard/CHART_OPTIONS.ts b/web/src/Pages/Dashboard/CHART_OPTIONS.ts index 9aa1a4a82..a725be10c 100644 --- a/web/src/Pages/Dashboard/CHART_OPTIONS.ts +++ b/web/src/Pages/Dashboard/CHART_OPTIONS.ts @@ -1,3 +1,5 @@ +import { addCommSepRound } from '../../Definitions/InterfacesAndType/programme.definitions'; + export const totalProgrammesOptions: any = { states: { normal: { @@ -61,7 +63,7 @@ export const totalProgrammesOptions: any = { }, labels: { formatter: (value: any) => { - return parseInt(value); + return addCommSepRound(value); }, }, }, @@ -197,7 +199,7 @@ export const totalProgrammesOptionsSub: any = { }, labels: { formatter: (value: any) => { - return parseInt(value); + return addCommSepRound(value); }, }, }, @@ -362,7 +364,7 @@ export const totalCreditsOptions: any = { }, labels: { formatter: (value: any) => { - return parseFloat(value).toFixed(2); + return addCommSepRound(value); }, }, }, @@ -495,7 +497,7 @@ export const totalCreditsCertifiedOptions: any = { }, labels: { formatter: (value: any) => { - return parseFloat(value).toFixed(2); + return addCommSepRound(value); }, }, }, From 6e6bcfb6f853fb4ac723854c2a15dfee522e2ff4 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Fri, 24 Feb 2023 16:35:59 +0530 Subject: [PATCH 06/68] installed nest-i18tn --- lambda/services/nest-cli.json | 2 +- lambda/services/package.json | 1 + lambda/services/src/i18n/en/test.json | 3 ++ .../src/national-api/national.api.module.ts | 53 ++++++++++--------- .../services/src/shared/auth/auth.service.ts | 1 + lambda/services/yarn.lock | 26 ++++++++- 6 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 lambda/services/src/i18n/en/test.json diff --git a/lambda/services/nest-cli.json b/lambda/services/nest-cli.json index 256648114..467f2f9ee 100644 --- a/lambda/services/nest-cli.json +++ b/lambda/services/nest-cli.json @@ -2,4 +2,4 @@ "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src" -} +} \ No newline at end of file diff --git a/lambda/services/package.json b/lambda/services/package.json index be9c89752..c077b3a8f 100644 --- a/lambda/services/package.json +++ b/lambda/services/package.json @@ -49,6 +49,7 @@ "jsbi": "^4.3.0", "nanoid": "3.3.4", "nest-winston": "^1.8.0", + "nestjs-i18n": "^10.2.6", "nodemailer": "^6.8.0", "passport": "^0.6.0", "passport-headerapikey": "^1.2.2", diff --git a/lambda/services/src/i18n/en/test.json b/lambda/services/src/i18n/en/test.json new file mode 100644 index 000000000..ca2e79542 --- /dev/null +++ b/lambda/services/src/i18n/en/test.json @@ -0,0 +1,3 @@ +{ + "here": "here" +} \ No newline at end of file diff --git a/lambda/services/src/national-api/national.api.module.ts b/lambda/services/src/national-api/national.api.module.ts index 78074b37f..2d06f235c 100644 --- a/lambda/services/src/national-api/national.api.module.ts +++ b/lambda/services/src/national-api/national.api.module.ts @@ -1,44 +1,49 @@ -import { Logger, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { NationalAPIController } from './national.api.controller'; -import { NationalAPIService } from './national.api.service'; -import configuration from '../shared/configuration'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { TypeOrmConfigService } from '../shared/typeorm.config.service'; +import { Logger, Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import * as path from "path"; +import { I18nModule } from "nestjs-i18n"; +import { NationalAPIController } from "./national.api.controller"; +import { NationalAPIService } from "./national.api.service"; +import configuration from "../shared/configuration"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { TypeOrmConfigService } from "../shared/typeorm.config.service"; // import { Programme } from './entities/programme.entity'; -import { AuthModule } from '../shared/auth/auth.module'; -import { CaslModule } from '../shared/casl/casl.module'; -import { ProgrammeModule } from '../shared/programme/programme.module'; -import { CompanyModule } from '../shared/company/company.module'; -import { CompanyController } from './company.controller'; -import { UserModule } from '../shared/user/user.module'; -import { UserController } from './user.controller'; -import { AuthController } from './auth.controller'; -import { ProgrammeController } from './programme.controller'; -import { UtilModule } from '../shared/util/util.module'; +import { AuthModule } from "../shared/auth/auth.module"; +import { CaslModule } from "../shared/casl/casl.module"; +import { ProgrammeModule } from "../shared/programme/programme.module"; +import { CompanyModule } from "../shared/company/company.module"; +import { CompanyController } from "./company.controller"; +import { UserModule } from "../shared/user/user.module"; +import { UserController } from "./user.controller"; +import { AuthController } from "./auth.controller"; +import { ProgrammeController } from "./programme.controller"; +import { UtilModule } from "../shared/util/util.module"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [configuration], - envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`] + envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`], }), TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService, - imports: undefined + imports: undefined, }), AuthModule, UserModule, CaslModule, ProgrammeModule, CompanyModule, - UtilModule + UtilModule, ], - controllers: [ NationalAPIController, UserController, AuthController, CompanyController, ProgrammeController ], - providers: [ - NationalAPIService, - Logger + controllers: [ + NationalAPIController, + UserController, + AuthController, + CompanyController, + ProgrammeController, ], + providers: [NationalAPIService, Logger], }) export class NationalAPIModule {} diff --git a/lambda/services/src/shared/auth/auth.service.ts b/lambda/services/src/shared/auth/auth.service.ts index 7cdbdc742..1a438d9de 100644 --- a/lambda/services/src/shared/auth/auth.service.ts +++ b/lambda/services/src/shared/auth/auth.service.ts @@ -54,6 +54,7 @@ export class AuthService { parseInt(organisationDetails.state) ); const ability = this.caslAbilityFactory.createForUser(user); + // return this.I18n.t("lang.here"); return { access_token: this.jwtService.sign(instanceToPlain(payload)), role: user.role, diff --git a/lambda/services/yarn.lock b/lambda/services/yarn.lock index eced90049..d848ab585 100644 --- a/lambda/services/yarn.lock +++ b/lambda/services/yarn.lock @@ -3336,6 +3336,11 @@ JSONStream@^1.0.3: jsonparse "^1.2.0" through ">=2.2.7 <3" +accept-language-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" + integrity sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw== + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -4845,7 +4850,7 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: +cookie@0.5.0, cookie@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== @@ -6946,7 +6951,7 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterare@1.2.1: +iterare@1.2.1, iterare@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== @@ -8260,6 +8265,18 @@ nest-winston@^1.8.0: dependencies: fast-safe-stringify "^2.1.1" +nestjs-i18n@^10.2.6: + version "10.2.6" + resolved "https://registry.yarnpkg.com/nestjs-i18n/-/nestjs-i18n-10.2.6.tgz#ea15e863731036ddd5f4eb5068c56dab1355ebdc" + integrity sha512-vWjOXNz3ohJcKybtgdCWruNqreNOkVGfbTzGFxxdZ6Y9VfVJERT2+/30KAcBwJTpjuStoTVhMpILAKkXMk8KLQ== + dependencies: + accept-language-parser "^1.5.0" + chokidar "^3.5.3" + cookie "^0.5.0" + iterare "^1.2.1" + js-yaml "^4.1.0" + string-format "^2.0.0" + next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -9858,6 +9875,11 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" From 657199993cd2a834d5ce46b679bedccda7f146e4 Mon Sep 17 00:00:00 2001 From: palindaa Date: Sat, 25 Feb 2023 22:41:26 +0530 Subject: [PATCH 07/68] add docker --- docker-compose.yml | 26 +++++++++++++++++++++ lambda/services/Dockerfile | 27 +++++++++++++++++++++ lambda/services/package.json | 1 + lambda/services/src/main.ts | 7 +++--- lambda/services/src/shared/server.ts | 35 ++++++++++++++++------------ 5 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 docker-compose.yml create mode 100644 lambda/services/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ec3e4bed8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.3' +services: + db: + container_name: db + image: postgres + restart: always + ports: + - '5432:5432' + environment: + POSTGRES_PASSWORD: '' + POSTGRES_USER: root + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: carbondev + national: + build: + context: . + dockerfile: ./lambda/services/Dockerfile + ports: + - '3000:3000' + depends_on: + - db + environment: + DB_HOST: db + DB_USER: root + + diff --git a/lambda/services/Dockerfile b/lambda/services/Dockerfile new file mode 100644 index 000000000..bf5c5af81 --- /dev/null +++ b/lambda/services/Dockerfile @@ -0,0 +1,27 @@ +# Base image +FROM node:alpine3.16 + +# Create app directory +WORKDIR /app/lambda/services + +COPY ./lambda/services/package.json ./ +COPY ./lambda/services/yarn.lock ./ + +COPY ./libs ../../libs + +# Build libraries +RUN yarn run sls:installProd +# Install app dependencies +# RUN yarn install --production --frozen-lockfile + +# Bundle app source +COPY ./lambda/services . + +# Creates a "dist" folder with the production build +RUN yarn run build +RUN yarn cache clean + +ENV NODE_ENV production + +# Start the server using the production build +CMD [ "node", "dist/main.js" ] \ No newline at end of file diff --git a/lambda/services/package.json b/lambda/services/package.json index be9c89752..456ffa2dd 100644 --- a/lambda/services/package.json +++ b/lambda/services/package.json @@ -10,6 +10,7 @@ "build": "nest build", "build:libs": "cd ../../libs/serial-number-gen/ && yarn install && yarn run build && cd ../../libs/carbon-credit-calculator && yarn install && yarn run devBuild && cd ../../lambda/services", "sls:install": "yarn run build:libs && cd ../../lambda/services && yarn install --frozen-lockfile", + "sls:installProd": "yarn run build:libs && cd ../../lambda/services && yarn install --production --frozen-lockfile", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", diff --git a/lambda/services/src/main.ts b/lambda/services/src/main.ts index cc4af7a52..bc8fafe46 100644 --- a/lambda/services/src/main.ts +++ b/lambda/services/src/main.ts @@ -1,10 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { AnalyticsAPIModule } from './analytics-api/analytics.api.module'; import { NationalAPIModule } from './national-api/national.api.module'; +import { buildNestApp } from './shared/server'; async function bootstrap() { - const app = await NestFactory.create(AnalyticsAPIModule); - app.setGlobalPrefix('/stats') - await app.listen(3000); + const app = await buildNestApp(process.env.RUN_MODULE || NationalAPIModule, '/national') + // await NestFactory.create(process.env.RUN_MODULE || NationalAPIModule); + await app.listen(process.env.RUN_PORT || 3000); } bootstrap(); diff --git a/lambda/services/src/shared/server.ts b/lambda/services/src/shared/server.ts index 9352b1221..3f70f4cf1 100644 --- a/lambda/services/src/shared/server.ts +++ b/lambda/services/src/shared/server.ts @@ -3,7 +3,7 @@ import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-win import { createServer, proxy } from 'aws-serverless-express'; import { eventContext } from 'aws-serverless-express/middleware'; -import { NestFactory } from '@nestjs/core'; +import { AbstractHttpAdapter, NestFactory } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import { Server } from 'http'; import * as winston from 'winston'; @@ -69,23 +69,28 @@ export function getLogger(module) { }); } +export async function buildNestApp(module: any, httpBase: string, expressApp?: AbstractHttpAdapter): Promise { + const nestApp = await NestFactory.create(module, new ExpressAdapter(expressApp), { + logger: getLogger(module), + }) + useContainer(nestApp.select(UtilModule), { fallbackOnErrors: true }); + nestApp.setGlobalPrefix(httpBase) + nestApp.use(bodyParser.json({limit: '50mb'})); + nestApp.enableCors(); + nestApp.useGlobalPipes(new TrimPipe()); + nestApp.useGlobalPipes(new ValidationPipe({ + exceptionFactory: (errors) => new ValidationException(errors) + })); + nestApp.useGlobalFilters(new ValidationExceptionFilter()) + nestApp.use(eventContext()); + setupSwagger(nestApp, module.name, httpBase) + return nestApp; +} + export async function bootstrapServer(cachedServer: Server, module: any, httpBase: string): Promise { if (!cachedServer) { const expressApp = express(); - const nestApp = await NestFactory.create(module, new ExpressAdapter(expressApp), { - logger: getLogger(module), - }) - useContainer(nestApp.select(UtilModule), { fallbackOnErrors: true }); - nestApp.setGlobalPrefix(httpBase) - nestApp.use(bodyParser.json({limit: '50mb'})); - nestApp.enableCors(); - nestApp.useGlobalPipes(new TrimPipe()); - nestApp.useGlobalPipes(new ValidationPipe({ - exceptionFactory: (errors) => new ValidationException(errors) - })); - nestApp.useGlobalFilters(new ValidationExceptionFilter()) - nestApp.use(eventContext()); - setupSwagger(nestApp, module.name, httpBase) + const nestApp = await buildNestApp(module, httpBase, expressApp); await nestApp.init(); cachedServer = createServer(expressApp, undefined, binaryMimeTypes); } From c273f55305929dba9eaf94ad624e7995c393ef13 Mon Sep 17 00:00:00 2001 From: palindaa Date: Sun, 26 Feb 2023 15:42:46 +0530 Subject: [PATCH 08/68] containerize the app --- .dockerignore | 1 + docker-compose.yml | 15 +++++++++++++++ lambda/services/.dockerignore | 1 + lambda/services/Dockerfile | 9 ++------- lambda/services/src/main.ts | 19 +++++++++++++++++-- 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 lambda/services/.dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..600e365ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/node_modules \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ec3e4bed8..b5e43e133 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,5 +22,20 @@ services: environment: DB_HOST: db DB_USER: root + RUN_MODULE: national-api + stats: + build: + context: . + dockerfile: ./lambda/services/Dockerfile + ports: + - '3100:3100' + depends_on: + - db + - national + environment: + DB_HOST: db + DB_USER: root + RUN_PORT: 3100 + RUN_MODULE: analytics-api diff --git a/lambda/services/.dockerignore b/lambda/services/.dockerignore new file mode 100644 index 000000000..600e365ec --- /dev/null +++ b/lambda/services/.dockerignore @@ -0,0 +1 @@ +**/node_modules \ No newline at end of file diff --git a/lambda/services/Dockerfile b/lambda/services/Dockerfile index bf5c5af81..86ab01d8a 100644 --- a/lambda/services/Dockerfile +++ b/lambda/services/Dockerfile @@ -9,19 +9,14 @@ COPY ./lambda/services/yarn.lock ./ COPY ./libs ../../libs -# Build libraries -RUN yarn run sls:installProd # Install app dependencies -# RUN yarn install --production --frozen-lockfile +RUN yarn run sls:installProd # Bundle app source COPY ./lambda/services . -# Creates a "dist" folder with the production build -RUN yarn run build -RUN yarn cache clean - ENV NODE_ENV production +RUN yarn add @nestjs/cli --dev && yarn run build && yarn remove @nestjs/cli && yarn cache clean # Start the server using the production build CMD [ "node", "dist/main.js" ] \ No newline at end of file diff --git a/lambda/services/src/main.ts b/lambda/services/src/main.ts index bc8fafe46..1142285c2 100644 --- a/lambda/services/src/main.ts +++ b/lambda/services/src/main.ts @@ -4,8 +4,23 @@ import { NationalAPIModule } from './national-api/national.api.module'; import { buildNestApp } from './shared/server'; async function bootstrap() { - const app = await buildNestApp(process.env.RUN_MODULE || NationalAPIModule, '/national') - // await NestFactory.create(process.env.RUN_MODULE || NationalAPIModule); + let module; + let httpPath; + switch (process.env.RUN_MODULE) { + case 'national-api': + module = NationalAPIModule; + httpPath = 'national' + break; + case 'analytics-api': + module = AnalyticsAPIModule; + httpPath = 'stats' + break; + default: + module = NationalAPIModule; + httpPath = 'national' + } + + const app = await buildNestApp(module, '/' + httpPath) await app.listen(process.env.RUN_PORT || 3000); } bootstrap(); From b78134f7f056d3fc694597d9a63fa51fe182c748 Mon Sep 17 00:00:00 2001 From: palindaa Date: Sun, 26 Feb 2023 19:27:52 +0530 Subject: [PATCH 09/68] initial commit Signed-off-by: palindaa --- lambda/services/src/setup/handler.ts | 4 +- .../src/shared/ledger-db/ledger-db.module.ts | 28 ++- .../ledger-db/ledger-db.service.spec.ts | 8 +- .../shared/ledger-db/ledger.db.interface.ts | 41 +++++ .../shared/ledger-db/pgsql-ledger.service.ts | 172 ++++++++++++++++++ ...r-db.service.ts => qldb-ledger.service.ts} | 101 +++++----- .../programme-ledger.service.ts | 11 +- 7 files changed, 284 insertions(+), 81 deletions(-) create mode 100644 lambda/services/src/shared/ledger-db/ledger.db.interface.ts create mode 100644 lambda/services/src/shared/ledger-db/pgsql-ledger.service.ts rename lambda/services/src/shared/ledger-db/{ledger-db.service.ts => qldb-ledger.service.ts} (73%) diff --git a/lambda/services/src/setup/handler.ts b/lambda/services/src/setup/handler.ts index d98b92fb4..d29a643f1 100644 --- a/lambda/services/src/setup/handler.ts +++ b/lambda/services/src/setup/handler.ts @@ -2,7 +2,7 @@ import { NestFactory } from "@nestjs/core"; import { Role } from "../shared/casl/role.enum"; import { UserDto } from "../shared/dto/user.dto"; import { LedgerDbModule } from "../shared/ledger-db/ledger-db.module"; -import { LedgerDbService } from "../shared/ledger-db/ledger-db.service"; +import { QLDBLedgerService } from "../shared/ledger-db/qldb-ledger.service"; import { getLogger } from "../shared/server"; import { UtilModule } from "../shared/util/util.module"; import { Country } from "../shared/entities/country.entity"; @@ -33,7 +33,7 @@ exports.handler = async (event) => { logger: getLogger(LedgerDbModule), }); try { - const ledgerModule = app.get(LedgerDbService) + const ledgerModule = app.get(QLDBLedgerService) await ledgerModule.createTable('company'); await ledgerModule.createIndex('txId', 'company'); diff --git a/lambda/services/src/shared/ledger-db/ledger-db.module.ts b/lambda/services/src/shared/ledger-db/ledger-db.module.ts index 36f92ddad..45744c096 100644 --- a/lambda/services/src/shared/ledger-db/ledger-db.module.ts +++ b/lambda/services/src/shared/ledger-db/ledger-db.module.ts @@ -1,22 +1,32 @@ -import { Logger, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import configuration from '../configuration'; -import { TypeOrmConfigService } from '../typeorm.config.service'; -import { LedgerDbService } from './ledger-db.service'; +import { Logger, Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import configuration from "../configuration"; +import { TypeOrmConfigService } from "../typeorm.config.service"; +import { LedgerDBInterface } from "./ledger.db.interface"; +import { QLDBLedgerService } from "./qldb-ledger.service"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [configuration], - envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`] + envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`], }), TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService, }), ], - providers: [LedgerDbService, Logger], - exports: [LedgerDbService] + providers: [ + Logger, + { + provide: LedgerDBInterface, + useClass: + process.env.LEDGER_TYPE === "QLDB" + ? QLDBLedgerService + : QLDBLedgerService, + }, + ], + exports: [LedgerDBInterface], }) export class LedgerDbModule {} diff --git a/lambda/services/src/shared/ledger-db/ledger-db.service.spec.ts b/lambda/services/src/shared/ledger-db/ledger-db.service.spec.ts index 9681fb27b..724ac178d 100644 --- a/lambda/services/src/shared/ledger-db/ledger-db.service.spec.ts +++ b/lambda/services/src/shared/ledger-db/ledger-db.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { LedgerDbService } from './ledger-db.service'; +import { QLDBLedgerService } from "./qldb-ledger.service"; describe('LedgerDbService', () => { - let service: LedgerDbService; + let service: QLDBLedgerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [LedgerDbService], + providers: [QLDBLedgerService], }).compile(); - service = module.get(LedgerDbService); + service = module.get(QLDBLedgerService); }); it('should be defined', () => { diff --git a/lambda/services/src/shared/ledger-db/ledger.db.interface.ts b/lambda/services/src/shared/ledger-db/ledger.db.interface.ts new file mode 100644 index 000000000..fa76d6a84 --- /dev/null +++ b/lambda/services/src/shared/ledger-db/ledger.db.interface.ts @@ -0,0 +1,41 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { dom } from "ion-js"; + +export class ArrayIn { + constructor(public key: string, public value: any) { + } +} + +export class ArrayLike { + constructor(public key: string, public value: any) { + } + + public toString = () : string => { + return `${this.value}`; + } +} + +@Injectable() +export abstract class LedgerDBInterface { + + public tableName: string; + public overallTableName: string; + public companyTableName: string; + public ledgerName: string; + + abstract createTable(tableName?: string): Promise; + + abstract createIndex(indexCol: string, tableName?: string): Promise; + + abstract insertRecord(document: Record, tableName?: string): Promise; + + abstract fetchRecords(where: Record): Promise; + + abstract fetchHistory(where: Record): Promise; + + abstract updateRecords(update: Record, where: Record): Promise; + + abstract getAndUpdateTx(getQueries: Record>, processGetFn: (results: Record) => [Record, Record, Record]): Promise>; + +} diff --git a/lambda/services/src/shared/ledger-db/pgsql-ledger.service.ts b/lambda/services/src/shared/ledger-db/pgsql-ledger.service.ts new file mode 100644 index 000000000..61b8e3bfb --- /dev/null +++ b/lambda/services/src/shared/ledger-db/pgsql-ledger.service.ts @@ -0,0 +1,172 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { dom } from "ion-js"; +import { ArrayIn, ArrayLike, LedgerDBInterface } from './ledger.db.interface'; +import { Pool } from "pg"; +// import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Programme } from '../entities/programme.entity'; + +// @Entity() +class MetaEntity { + // @PrimaryGeneratedColumn() + version: number; + + // @CreateDateColumn() + txTime: Date; +} + +// @Entity() +export class PgProgrammeEventEntity { + // @Column({ + // type: "jsonb", + // array: false, + // }) + data: Programme; + + // @Column({ + // type: "jsonb", + // array: false, + // }) + meta: MetaEntity; + + // @PrimaryGeneratedColumn() + hash: number; +} + +@Injectable() +export class PgSqlLedgerService implements LedgerDBInterface { + + public tableName: string; + public overallTableName: string; + public companyTableName: string; + public ledgerName: string; + private dbCon: Pool; + + constructor(private readonly logger: Logger, private readonly configService: ConfigService) { + this.ledgerName = configService.get('ledger.name'); + this.tableName = configService.get('ledger.table'); + this.overallTableName = configService.get('ledger.overallTable'); + this.companyTableName = configService.get('ledger.companyTable'); + logger.log("PgSQL Ledger init ", this.ledgerName); + } + getAndUpdateTx(getQueries: Record>, processGetFn: (results: Record) => [Record, Record, Record]): Promise> { + throw new Error('Method not implemented.'); + } + + // TODO: Handler session expire + private async execute(sql, ...parameters: any[]): Promise { + this.logger.debug(`Statement: ${sql}, parameter: ${JSON.stringify(parameters)}`); + + let dbConfig = this.configService.get('database'); + dbConfig['database'] = dbConfig['database'] + 'Events'; + this.dbCon = new Pool() + const resp = await this.dbCon.query( + sql, + ...parameters + ); + this.logger.debug('Response', JSON.stringify(resp)); + this.dbCon.close(); + return resp; + } + + public async createTable(tableName?: string): Promise { + const sql = `CREATE TABLE IF NOT EXISTS ${tableName ? tableName : this.tableName} + ( + data jsonb NOT NULL, + meta jsonb NOT NULL, + hash integer NOT NULL DEFAULT nextval('${tableName ? tableName : this.tableName}_hash_seq'::regclass), + PRIMARY KEY (hash) + )` + await (await this.execute(sql)); + } + + public async createIndex(indexCol: string, tableName?: string): Promise { + return null; + // await (await this.execute(`create index on ${tableName ? tableName : this.tableName} (${indexCol})`)); + } + + public async insertRecord(document: Record, tableName?: string): Promise { + await (await this.execute(`INSERT INTO ${tableName ? tableName : this.tableName} ?`, document)); + } + + public async fetchRecords(where: Record): Promise { + const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and '); + return (await this.execute(`SELECT * FROM ${this.tableName} WHERE ${whereClause}`, ...Object.values(where)))?.getResultList(); + } + + public async fetchHistory(where: Record): Promise { + const whereClause = Object.keys(where).map(k => (`h.data.${k} = ?`)).join(' and '); + const x = (await this.execute(`SELECT * FROM history(${this.tableName}) as h WHERE ${whereClause}`, ...Object.values(where)))?.getResultList(); + console.log('Results', x); + return x; + } + + public async updateRecords(update: Record, where: Record): Promise { + const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and '); + const updateClause = Object.keys(update).map(k => (`${k} = ?`)).join(','); + return (await this.execute(`UPDATE ${this.tableName} SET ${updateClause} WHERE ${whereClause}`, ...Object.values(update), ...Object.values(where)))?.getResultList(); + }; + + private getValuesList(filterObj: any): any { + const list = []; + for (const k in filterObj) { + const v = filterObj[k]; + if (v instanceof ArrayIn) { + list.push(v.value); + } else if (v instanceof ArrayLike) { + list.push(v.value); + } else { + list.push(v); + } + } + return list; + } + + // public async getAndUpdateTx(getQueries: Record>, processGetFn: (results: Record) => [Record, Record, Record]): Promise> { + // this.logger.debug(``); + // this.driver = new QldbDriver(this.ledgerName); + // const resp = await this.driver.executeLambda(async (txn: TransactionExecutor) => { + // const getResults = {}; + // for (const t in getQueries) { + // if (getQueries.hasOwnProperty(t)) { + // const wc = Object.keys(getQueries[t]).map(k => { + // if (getQueries[t][k] instanceof Array) { + // return (`${k} in ?`); + // } else if (getQueries[t][k] instanceof ArrayIn) { + // return (`? IN "${getQueries[t][k].key}"`); + // } else if (getQueries[t][k] instanceof ArrayLike) { + // return (`${k} LIKE ?`); + // } + // return (`${k} = ?`); + // }).join(' and '); + // const r = (await this.execute(`SELECT * FROM ${t} WHERE ${wc}`, ...this.getValuesList(getQueries[t])))?.getResultList(); + // getResults[t] = r; + // } + // } + // const [update, updateWhere, insert] = processGetFn(getResults); + // const updateResults = {}; + // for (const qk in update) { + // const tableName = qk.split('#')[0]; + // if (update.hasOwnProperty(qk) && updateWhere.hasOwnProperty(qk)) { + // const whereClause = Object.keys(updateWhere[qk]).map(k => (`${k} = ?`)).join(' and '); + // const updateClause = Object.keys(update[qk]).map(k => (`${k} = ?`)).join(','); + // updateResults[qk] = (await this.execute(`UPDATE ${tableName} SET ${updateClause} WHERE ${whereClause}`, ...Object.values(update[qk]), ...Object.values(updateWhere[qk])))?.getResultList(); + // } + // } + + // this.logger.verbose(`Insert queries`, JSON.stringify(insert)); + // for (const qk in insert) { + // const tableName = qk.split('#')[0]; + // if (insert.hasOwnProperty(qk)) { + // updateResults[qk] = (await this.execute(`INSERT INTO ${tableName ? tableName : this.tableName} ?`, insert[qk]))?.getResultList(); + // } + // } + + // return updateResults; + // }); + // this.logger.debug('Response', JSON.stringify(resp)); + // this.driver.close(); + // return resp; + // } + +} diff --git a/lambda/services/src/shared/ledger-db/ledger-db.service.ts b/lambda/services/src/shared/ledger-db/qldb-ledger.service.ts similarity index 73% rename from lambda/services/src/shared/ledger-db/ledger-db.service.ts rename to lambda/services/src/shared/ledger-db/qldb-ledger.service.ts index 99ce49e5b..e8e83657a 100644 --- a/lambda/services/src/shared/ledger-db/ledger-db.service.ts +++ b/lambda/services/src/shared/ledger-db/qldb-ledger.service.ts @@ -2,52 +2,39 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; import { dom } from "ion-js"; - -export class ArrayIn { - constructor(public key: string, public value: any) { - } -} - -export class ArrayLike { - constructor(public key: string, public value: any) { - } - - public toString = () : string => { - return `${this.value}`; - } -} +import { ArrayIn, ArrayLike, LedgerDBInterface } from './ledger.db.interface'; @Injectable() -export class LedgerDbService { +export class QLDBLedgerService implements LedgerDBInterface { public tableName: string; public overallTableName: string; public companyTableName: string; - private ledgerName: string; + public ledgerName: string; private driver: QldbDriver; constructor(private readonly logger: Logger, private readonly configService: ConfigService) { - this.ledgerName = configService.get('ledger.name') - this.tableName = configService.get('ledger.table') - this.overallTableName = configService.get('ledger.overallTable') - this.companyTableName = configService.get('ledger.companyTable') - logger.log("Ledger init ", this.ledgerName) + this.ledgerName = configService.get('ledger.name'); + this.tableName = configService.get('ledger.table'); + this.overallTableName = configService.get('ledger.overallTable'); + this.companyTableName = configService.get('ledger.companyTable'); + logger.log("QLDB Ledger init ", this.ledgerName); } // TODO: Handler session expire - public async execute(sql, ...parameters: any[]): Promise { - this.logger.debug(`Statement: ${sql}, parameter: ${JSON.stringify(parameters)}`) + private async execute(sql, ...parameters: any[]): Promise { + this.logger.debug(`Statement: ${sql}, parameter: ${JSON.stringify(parameters)}`); this.driver = new QldbDriver(this.ledgerName); const resp = await this.driver.executeLambda(async (txn: TransactionExecutor) => { if (parameters.length > 0) { - return await txn.execute(sql, ...parameters) + return await txn.execute(sql, ...parameters); } else { - return await txn.execute(sql) + return await txn.execute(sql); } - + }); - this.logger.debug('Response', JSON.stringify(resp)) - this.driver.close() + this.logger.debug('Response', JSON.stringify(resp)); + this.driver.close(); return resp; } @@ -62,75 +49,75 @@ export class LedgerDbService { public async insertRecord(document: Record, tableName?: string): Promise { await (await this.execute(`INSERT INTO ${tableName ? tableName : this.tableName} ?`, document)); } - + public async fetchRecords(where: Record): Promise { - const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and ') + const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and '); return (await this.execute(`SELECT * FROM ${this.tableName} WHERE ${whereClause}`, ...Object.values(where)))?.getResultList(); } public async fetchHistory(where: Record): Promise { - const whereClause = Object.keys(where).map(k => (`h.data.${k} = ?`)).join(' and ') + const whereClause = Object.keys(where).map(k => (`h.data.${k} = ?`)).join(' and '); const x = (await this.execute(`SELECT * FROM history(${this.tableName}) as h WHERE ${whereClause}`, ...Object.values(where)))?.getResultList(); - console.log('Results', x) - return x + console.log('Results', x); + return x; } - - public async updateRecords(update: Record, where: Record): Promise { - const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and ') - const updateClause = Object.keys(update).map(k => (`${k} = ?`)).join(',') + + public async updateRecords(update: Record, where: Record): Promise { + const whereClause = Object.keys(where).map(k => (`${k} = ?`)).join(' and '); + const updateClause = Object.keys(update).map(k => (`${k} = ?`)).join(','); return (await this.execute(`UPDATE ${this.tableName} SET ${updateClause} WHERE ${whereClause}`, ...Object.values(update), ...Object.values(where)))?.getResultList(); }; private getValuesList(filterObj: any): any { - const list = [] + const list = []; for (const k in filterObj) { const v = filterObj[k]; if (v instanceof ArrayIn) { - list.push(v.value) + list.push(v.value); } else if (v instanceof ArrayLike) { - list.push(v.value) + list.push(v.value); } else { - list.push(v) + list.push(v); } } return list; } public async getAndUpdateTx(getQueries: Record>, processGetFn: (results: Record) => [Record, Record, Record]): Promise> { - this.logger.debug(``) + this.logger.debug(``); this.driver = new QldbDriver(this.ledgerName); const resp = await this.driver.executeLambda(async (txn: TransactionExecutor) => { - const getResults = {} + const getResults = {}; for (const t in getQueries) { if (getQueries.hasOwnProperty(t)) { const wc = Object.keys(getQueries[t]).map(k => { if (getQueries[t][k] instanceof Array) { - return (`${k} in ?`) + return (`${k} in ?`); } else if (getQueries[t][k] instanceof ArrayIn) { - return (`? IN "${getQueries[t][k].key}"`) + return (`? IN "${getQueries[t][k].key}"`); } else if (getQueries[t][k] instanceof ArrayLike) { - return (`${k} LIKE ?`) + return (`${k} LIKE ?`); } - return (`${k} = ?`) - }).join(' and ') + return (`${k} = ?`); + }).join(' and '); const r = (await this.execute(`SELECT * FROM ${t} WHERE ${wc}`, ...this.getValuesList(getQueries[t])))?.getResultList(); getResults[t] = r; } } - const [update, updateWhere, insert] = processGetFn(getResults); - const updateResults = {} + const [update, updateWhere, insert] = processGetFn(getResults); + const updateResults = {}; for (const qk in update) { - const tableName = qk.split('#')[0] + const tableName = qk.split('#')[0]; if (update.hasOwnProperty(qk) && updateWhere.hasOwnProperty(qk)) { - const whereClause = Object.keys(updateWhere[qk]).map(k => (`${k} = ?`)).join(' and ') - const updateClause = Object.keys(update[qk]).map(k => (`${k} = ?`)).join(',') + const whereClause = Object.keys(updateWhere[qk]).map(k => (`${k} = ?`)).join(' and '); + const updateClause = Object.keys(update[qk]).map(k => (`${k} = ?`)).join(','); updateResults[qk] = (await this.execute(`UPDATE ${tableName} SET ${updateClause} WHERE ${whereClause}`, ...Object.values(update[qk]), ...Object.values(updateWhere[qk])))?.getResultList(); } } - this.logger.verbose(`Insert queries`, JSON.stringify(insert)) + this.logger.verbose(`Insert queries`, JSON.stringify(insert)); for (const qk in insert) { - const tableName = qk.split('#')[0] + const tableName = qk.split('#')[0]; if (insert.hasOwnProperty(qk)) { updateResults[qk] = (await this.execute(`INSERT INTO ${tableName ? tableName : this.tableName} ?`, insert[qk]))?.getResultList(); } @@ -138,8 +125,8 @@ export class LedgerDbService { return updateResults; }); - this.logger.debug('Response', JSON.stringify(resp)) - this.driver.close() + this.logger.debug('Response', JSON.stringify(resp)); + this.driver.close(); return resp; } diff --git a/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts b/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts index 3e8e36bdd..877d9555e 100644 --- a/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts +++ b/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts @@ -7,26 +7,19 @@ import axios from "axios"; import { generateSerialNumber } from "serial-number-gen"; import { EntityManager } from "typeorm"; import { ProgrammeHistoryDto } from "../dto/programme.history.dto"; -import { ProgrammeTransferApprove } from "../dto/programme.transfer.approve"; -import { Company } from "../entities/company.entity"; import { CreditOverall } from "../entities/credit.overall.entity"; import { Programme } from "../entities/programme.entity"; import { ProgrammeTransfer } from "../entities/programme.transfer"; import { TxType } from "../enum/txtype.enum"; -import { - ArrayIn, - ArrayLike, - LedgerDbService, -} from "../ledger-db/ledger-db.service"; import { ProgrammeStage } from "../../shared/enum/programme-status.enum"; -import { User } from "../entities/user.entity"; +import { ArrayIn, ArrayLike, LedgerDBInterface } from "../ledger-db/ledger.db.interface"; @Injectable() export class ProgrammeLedgerService { constructor( private readonly logger: Logger, @InjectEntityManager() private entityManger: EntityManager, - private ledger: LedgerDbService + private ledger: LedgerDBInterface ) {} async forwardGeocoding(address: any[]) { From b4ad4b3050b70bbf5124fa0b271425130079fe74 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 10:57:58 +0530 Subject: [PATCH 10/68] fixed path issue in the national.module regarding the localization lib --- lambda/services/nest-cli.json | 11 ++++- .../src/national-api/national.api.module.ts | 13 +++++- .../programme.definitions.tsx | 4 ++ web/src/Pages/Dashboard/dashboard.tsx | 46 ++++++++++--------- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/lambda/services/nest-cli.json b/lambda/services/nest-cli.json index 467f2f9ee..d82af8189 100644 --- a/lambda/services/nest-cli.json +++ b/lambda/services/nest-cli.json @@ -1,5 +1,14 @@ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "assets": [ + { + "include": "i18n/**/*", + "watchAssets": true, + "outDir": ".build/src" + } + ] + } } \ No newline at end of file diff --git a/lambda/services/src/national-api/national.api.module.ts b/lambda/services/src/national-api/national.api.module.ts index 2d06f235c..07cfb235f 100644 --- a/lambda/services/src/national-api/national.api.module.ts +++ b/lambda/services/src/national-api/national.api.module.ts @@ -1,7 +1,7 @@ import { Logger, Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import * as path from "path"; -import { I18nModule } from "nestjs-i18n"; +import { AcceptLanguageResolver, I18nModule, QueryResolver } from "nestjs-i18n"; import { NationalAPIController } from "./national.api.controller"; import { NationalAPIService } from "./national.api.service"; import configuration from "../shared/configuration"; @@ -30,6 +30,17 @@ import { UtilModule } from "../shared/util/util.module"; useClass: TypeOrmConfigService, imports: undefined, }), + I18nModule.forRoot({ + fallbackLanguage: "en", + loaderOptions: { + path: path.join(__dirname, "../i18n/"), + watch: true, + }, + resolvers: [ + { use: QueryResolver, options: ["lang"] }, + AcceptLanguageResolver, + ], + }), AuthModule, UserModule, CaslModule, diff --git a/web/src/Definitions/InterfacesAndType/programme.definitions.tsx b/web/src/Definitions/InterfacesAndType/programme.definitions.tsx index 2a4aaaf0d..2f9f3fe3c 100644 --- a/web/src/Definitions/InterfacesAndType/programme.definitions.tsx +++ b/web/src/Definitions/InterfacesAndType/programme.definitions.tsx @@ -227,6 +227,10 @@ export const addCommSepRound = (value: any) => { .replace(/\B(?=(\d{3})+(?!\d))/g, ','); }; +export const addRoundNumber = (value: any) => { + return Number(value.toFixed(2).replace('.00', '')); +}; + export const addSpaces = (text: string) => { if (!text) { return text; diff --git a/web/src/Pages/Dashboard/dashboard.tsx b/web/src/Pages/Dashboard/dashboard.tsx index 919d15f22..0075848d8 100644 --- a/web/src/Pages/Dashboard/dashboard.tsx +++ b/web/src/Pages/Dashboard/dashboard.tsx @@ -15,7 +15,11 @@ import ProgrammeRejectAndTransfer from './ProgrammeRejectAndTransfer'; import moment from 'moment'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; import mapboxgl from 'mapbox-gl'; -import { addCommSep } from '../../Definitions/InterfacesAndType/programme.definitions'; +import { + addCommSep, + addCommSepRound, + addRoundNumber, +} from '../../Definitions/InterfacesAndType/programme.definitions'; import { ClockHistory, BoxArrowInRight, @@ -1193,16 +1197,12 @@ const Dashboard = () => { } if (programmeByStatusAuthAggregationResponse?.length > 0) { programmeByStatusAuthAggregationResponse?.map((responseItem: any) => { - totalEstCredits = totalEstCredits + Math.round(parseFloat(responseItem?.totalestcredit)); - totalIssuedCredits = - totalIssuedCredits + Math.round(parseFloat(responseItem?.totalissuedcredit)); - totalRetiredCredits = - totalRetiredCredits + Math.round(parseFloat(responseItem?.totalretiredcredit)); - totalBalancecredit = - totalBalancecredit + Math.round(parseFloat(responseItem?.totalbalancecredit)); - totalTxCredits = totalTxCredits + Math.round(parseFloat(responseItem?.totaltxcredit)); - totalFrozenCredits = - totalFrozenCredits + Math.round(parseFloat(responseItem?.totalfreezecredit)); + totalEstCredits = totalEstCredits + parseFloat(responseItem?.totalestcredit); + totalIssuedCredits = totalIssuedCredits + parseFloat(responseItem?.totalissuedcredit); + totalRetiredCredits = totalRetiredCredits + parseFloat(responseItem?.totalretiredcredit); + totalBalancecredit = totalBalancecredit + parseFloat(responseItem?.totalbalancecredit); + totalTxCredits = totalTxCredits + parseFloat(responseItem?.totaltxcredit); + totalFrozenCredits = totalFrozenCredits + parseFloat(responseItem?.totalfreezecredit); }); } if (certifiedRevokedAggregationResponse) { @@ -1212,18 +1212,22 @@ const Dashboard = () => { } setCreditBalance(parseFloat(response?.data?.stats?.CREDIT_STATS_BALANCE?.sum)); const creditAuthorized = totalEstCredits - totalIssuedCredits; - pieSeriesCreditsData.push(creditAuthorized); + console.error('add comp func error --- > ', addCommSepRound(creditAuthorized)); + pieSeriesCreditsData.push(addRoundNumber(creditAuthorized)); pieSeriesCreditsData.push( - totalIssuedCredits - totalTxCredits - totalRetiredCredits - totalFrozenCredits + addRoundNumber( + totalIssuedCredits - totalTxCredits - totalRetiredCredits - totalFrozenCredits + ) + ); + pieSeriesCreditsData.push(addRoundNumber(totalTxCredits)); + pieSeriesCreditsData.push(addRoundNumber(totalRetiredCredits)); + + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalCertifiedCredit)); + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalUnCertifiedredit)); + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalRevokedCredits)); + const totalCreditsCertified = addRoundNumber( + totalCertifiedCredit + totalUnCertifiedredit + totalRevokedCredits ); - pieSeriesCreditsData.push(totalTxCredits); - pieSeriesCreditsData.push(totalRetiredCredits); - - pieSeriesCreditsCerifiedData.push(totalCertifiedCredit); - pieSeriesCreditsCerifiedData.push(totalUnCertifiedredit); - pieSeriesCreditsCerifiedData.push(totalRevokedCredits); - const totalCreditsCertified = - totalCertifiedCredit + totalUnCertifiedredit + totalRevokedCredits; setCreditsPieChartTotal( String(addCommSep(totalEstCredits)) !== 'NaN' ? addCommSep(totalEstCredits) : 0 ); From 971f2d52173bf7cf95690ff5ac71a509c349bc57 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 12:27:02 +0530 Subject: [PATCH 11/68] added json files regarding the success and error messages in the backend for english --- lambda/services/nest-cli.json | 5 ++ lambda/services/src/i18n/en/addUser.json | 10 ++++ .../src/i18n/en/creditCertification.json | 7 +++ .../src/i18n/en/creditRetirement.json | 7 +++ .../services/src/i18n/en/creditTransfers.json | 11 +++++ lambda/services/src/i18n/en/deleteUser.json | 5 ++ .../src/i18n/en/programmeAuthorisation.json | 6 +++ .../services/src/i18n/en/resetPassword.json | 5 ++ lambda/services/src/i18n/en/test.json | 3 -- .../src/national-api/auth.controller.ts | 49 ++++++++++++------- 10 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 lambda/services/src/i18n/en/addUser.json create mode 100644 lambda/services/src/i18n/en/creditCertification.json create mode 100644 lambda/services/src/i18n/en/creditRetirement.json create mode 100644 lambda/services/src/i18n/en/creditTransfers.json create mode 100644 lambda/services/src/i18n/en/deleteUser.json create mode 100644 lambda/services/src/i18n/en/programmeAuthorisation.json create mode 100644 lambda/services/src/i18n/en/resetPassword.json delete mode 100644 lambda/services/src/i18n/en/test.json diff --git a/lambda/services/nest-cli.json b/lambda/services/nest-cli.json index d82af8189..265aedcef 100644 --- a/lambda/services/nest-cli.json +++ b/lambda/services/nest-cli.json @@ -4,6 +4,11 @@ "sourceRoot": "src", "compilerOptions": { "assets": [ + { + "include": "i18n/**/*", + "watchAssets": true, + "outDir": "dist" + }, { "include": "i18n/**/*", "watchAssets": true, diff --git a/lambda/services/src/i18n/en/addUser.json b/lambda/services/src/i18n/en/addUser.json new file mode 100644 index 000000000..57af43ffe --- /dev/null +++ b/lambda/services/src/i18n/en/addUser.json @@ -0,0 +1,10 @@ +{ + "rootCreatesRoot": "There can be only one root user", + "createUserSuccess": "The user has been created successfully", + "createOrganisationSuccess": "The organisation has been created successfully", + "createExistingUser": "This user already exists", + "addUserToUnRegisteredCompany": "The programme ID is invalid", + "createManagerOrViewerAsInitialUserOfACompany": "The initial user must be an admin", + "createUserUnAuthorized": "This action is unauthorised", + "createUserToOtherCompaniesUnAuth": "This action is unauthorised" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/creditCertification.json b/lambda/services/src/i18n/en/creditCertification.json new file mode 100644 index 000000000..56764970c --- /dev/null +++ b/lambda/services/src/i18n/en/creditCertification.json @@ -0,0 +1,7 @@ +{ + "certifyPendingProgramme": "The programme has been certified successfully", + "certifyRejectedProgramme": "This action is unauthorised", + "certifyProgrammeAlreadyCertifiedBySameCertifier": "This programme has already been certified", + "unAuthorizedCertification": "This action is unauthorised", + "certificationRevocation": "The certificate has been revoked successfully" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/creditRetirement.json b/lambda/services/src/i18n/en/creditRetirement.json new file mode 100644 index 000000000..76ce6abce --- /dev/null +++ b/lambda/services/src/i18n/en/creditRetirement.json @@ -0,0 +1,7 @@ +{ + "retirementReSentSuccess": "The international transfer retire request has been sent successfully", + "retiredSuccess": "The credits have been retired successfully", + "retirementReqCancellationSuccess": "The international transfer retire request has been cancelled successfully", + "transferReqRejectedSuccess": "The international transfer retire request has been rejected successfully", + "transferReqAcceptSuccess": "The international transfer retire request has been accepted successfully" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/creditTransfers.json b/lambda/services/src/i18n/en/creditTransfers.json new file mode 100644 index 000000000..60c759ea5 --- /dev/null +++ b/lambda/services/src/i18n/en/creditTransfers.json @@ -0,0 +1,11 @@ +{ + "transferReqSentSuccess": "The transfer request has been sent successfully", + "transferReqCancelledSuccess": "The transfer request has been cancelled successfully", + "transferReqRejectSuccess": "The transfer request has been rejected successfully", + "transferReqAcceptSuccess": "The transfer request has been accepted successfully", + "rejectAlreadyRejectedTransferReq": "This transfer request has already been rejected", + "acceptAlreadyAcceptedTransferReq": "This transfer request has already been accepted", + "cancelAlreadyCancelledTransferReq": "This transfer request has already been cancelled", + "acceptAlreadyCancelledTransferReq": "This transfer request has been cancelled", + "rejectAlreadyCancelledTransferReq": "This transfer request has been cancelled" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/deleteUser.json b/lambda/services/src/i18n/en/deleteUser.json new file mode 100644 index 000000000..9146b0a03 --- /dev/null +++ b/lambda/services/src/i18n/en/deleteUser.json @@ -0,0 +1,5 @@ +{ + "deleteUserSuccess": "The user has been deleted successfully", + "deleteOneAdminWhenOnlyOneAdmin": "The user cannot be deleted as the user is the only admin", + "userDeletionUnAUth": "This action is unauthorised" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/programmeAuthorisation.json b/lambda/services/src/i18n/en/programmeAuthorisation.json new file mode 100644 index 000000000..a9243813f --- /dev/null +++ b/lambda/services/src/i18n/en/programmeAuthorisation.json @@ -0,0 +1,6 @@ +{ + "authoriseProgrammeSuccess": "The programme has been authorised successfully", + "authoriseAlreadyAuthorisedProg": "This programme has already been auhtorised", + "rejectProgrammeSuccess": "The programme has been rejected successfully", + "rejectAlreadyRejectedProg": "This programme has already been rejected" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/resetPassword.json b/lambda/services/src/i18n/en/resetPassword.json new file mode 100644 index 000000000..4260cb738 --- /dev/null +++ b/lambda/services/src/i18n/en/resetPassword.json @@ -0,0 +1,5 @@ +{ + "resetSuccess": "The password has been reset successfully", + "samePasswordReset": "New password cannot be the same as your old password", + "incorrectCurrentPassword": "Entered old password is incorrect" +} \ No newline at end of file diff --git a/lambda/services/src/i18n/en/test.json b/lambda/services/src/i18n/en/test.json deleted file mode 100644 index ca2e79542..000000000 --- a/lambda/services/src/i18n/en/test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "here": "here" -} \ No newline at end of file diff --git a/lambda/services/src/national-api/auth.controller.ts b/lambda/services/src/national-api/auth.controller.ts index ab676b198..98de5b529 100644 --- a/lambda/services/src/national-api/auth.controller.ts +++ b/lambda/services/src/national-api/auth.controller.ts @@ -1,24 +1,35 @@ -import { Controller, Get, Post, UseGuards, Request, Logger, Body, ValidationPipe, UnauthorizedException } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { LoginDto } from '../shared/dto/login.dto'; -import { AuthService } from '../shared/auth/auth.service'; -import { JwtAuthGuard } from '../shared/auth/guards/jwt-auth.guard'; -import { LocalAuthGuard } from '../shared/auth/guards/local-auth.guard'; +import { + Controller, + Get, + Post, + UseGuards, + Request, + Logger, + Body, + ValidationPipe, + UnauthorizedException, +} from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { I18n, I18nContext } from "nestjs-i18n"; +import { LoginDto } from "../shared/dto/login.dto"; +import { AuthService } from "../shared/auth/auth.service"; +import { JwtAuthGuard } from "../shared/auth/guards/jwt-auth.guard"; +import { LocalAuthGuard } from "../shared/auth/guards/local-auth.guard"; -@ApiTags('Auth') -@Controller('auth') +@ApiTags("Auth") +@Controller("auth") export class AuthController { + constructor(private readonly authService: AuthService) {} - constructor(private readonly authService: AuthService) {} - - @Post('login') - async login(@Body() login: LoginDto) { - const user = await this.authService.validateUser(login.username, login.password); - if (user != null) { - - return this.authService.login(user); - } - throw new UnauthorizedException(); + @Post("login") + async login(@Body() login: LoginDto, @I18n() i18n: I18nContext) { + const user = await this.authService.validateUser( + login.username, + login.password + ); + if (user != null) { + return this.authService.login(user); } - + throw new UnauthorizedException(); + } } From 91dc585b430d1742bbe86bdc2b4c97804ed8e5b1 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 12:30:24 +0530 Subject: [PATCH 12/68] minor change in auth controller file --- lambda/services/src/national-api/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/services/src/national-api/auth.controller.ts b/lambda/services/src/national-api/auth.controller.ts index 98de5b529..26944f2d3 100644 --- a/lambda/services/src/national-api/auth.controller.ts +++ b/lambda/services/src/national-api/auth.controller.ts @@ -22,7 +22,7 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @Post("login") - async login(@Body() login: LoginDto, @I18n() i18n: I18nContext) { + async login(@Body() login: LoginDto) { const user = await this.authService.validateUser( login.username, login.password From 7eddb683f2493de7867dccf4320c279e3b94ffd2 Mon Sep 17 00:00:00 2001 From: palindaa Date: Mon, 27 Feb 2023 14:37:51 +0530 Subject: [PATCH 13/68] Update drawio file --- documention/Carbon Registry Architecture.drawio | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documention/Carbon Registry Architecture.drawio b/documention/Carbon Registry Architecture.drawio index a399ec526..ce9124b9d 100644 --- a/documention/Carbon Registry Architecture.drawio +++ b/documention/Carbon Registry Architecture.drawio @@ -1 +1 @@ -7b1Zl6NG1jX8a7zW9124FvNwCUKAJAQIJIG48WKe55lf/0ZkZdaQmWW7u6tc7n5cdmUlwRTD2fvscwiCX/BduUid2yTnOgiLXzAkWH7BhV8wjCUJ8BMWrB8LMJTFPpbEXRp8LEM/F5jpFj4XIs+lYxqE/VcHDnVdDGnzdaFfV1XoD1+VuV1Xz18fFtXF13dt3Dh8U2D6bvG21EqDIXkuRSn28w45TOPk+dYMRn/cUbovBz+3pE/coJ6/KML3v+C7rq6Hj7+Vyy4sYOe99MvH88Rv7P1UsS6shj9zQt74RX7srNQTKnZnh8jRo399HozJLcbnBithEIcdKBPcwfXcPnyu/bC+dElXj1UQwquiv+D8nKRDaDauD/fOwAhAWTKUxfPuKeyGFHQnV6RxBcqGGh7wfFewL1y+2Rz0UycB6wrrMhy6FRzyfAJOPvfrs2X9Sr+YzPx5nLCXg5IvhghFngvdZ9uIP138c/eBX5578P3evO03/cZx2qETf017JiZ3dPsrSpNv+vMXjCrAjXkP9CkVD58a/0WHNnVaDU91IXnwP6jdDvmFBHt2cOsDRr4qeL1Nf12Avt2C1/i64PU2/XUB+vry6Kv7o68r+EXBm62vLo+8uj/yRQXB/zhfj0ORVuHuE6IRUBh3bpAC09jVRd2Bsqquwq8t7T1DjOpqeOYUFHvZfu54eFWAyQb+Xi4xpK8P7twTH2Jg4M3TLQ+AVd7d+xv49Te/qMfgN/dpdPuhq/PwpXK/YDj4T4SWw0dpUbyq9PuYcJ+3ijB6uiJoSVrFytOWgCPPtf/iFrudKLIsKA/cPvmEx8L1woJ3/Tx+wukXx0dPf8AhoFWDC3q4e+6FJ/sLu/0UfjRD5OmYonCbPvU+9VUX+mPXp1NohP3HLkU+GfMb2L4D7m8iGX2NZAp5i2SaeAfJOPodkDzelLT5dRRucVwMg/2baVryryj7BqWQF19sp+6GpI7ryi32n0v5z8QIe+bzMUoNB/hpdLJwGNZni3THof7ahEEfdqv9fP7TxgNuALA8bwrLlzuF9cstPexS0PhPo/rKWlhuR/Pi741YX4+d/8LxUc/jOv8YKc9YJrI71rvt1+c+GdwuDoff6bxnDwg77HfHvwsLdwAG9bXT/g+G8vdq/QUlQ88GSsyhC93yzTj3eTj4yZfQeE3N79LzexT9Lk2/peqvDnsiz3fu8LrwvTL6bSH69rAXvn1b+F7Ze87l9dnoO2ejr87+NrW/MtRPzPma8qERE6QgYl/sE1JAS0P6RNMVRNwrxgXnkByO8OR7HP2JED8RKPIFOyuQSPW6T58v79XDUJd/SN9+WD1h8Etc/5EbcvvmY3dE6QLr8b5f6sKPCP3olQAf9+/5pxz0b5/2vwXAyH/rn0y8/9T4FwmLfh/mJgjsa+ZmcPoD+Ya7Keotdb+UfXe4o2/gflGEt5rrH5T/fVEu0sweIf41lAsIuUPp/zMob0e3Gsbyt+IpWnsC+1Os9l1gjdPoK0HGoG9A/QL9L0H9Uvbd5RiOvAHw/yk59o1ok/6Tgoz43oLs6VSu69z1iwOeWfTzlXVY8IVZEa90Pk4yryzj4yU/28mnuv0nMTn9xiOYQ93BvM9rm/qPiOs5uHvFSxhNoyj1hpeeD/7bUdLrUBD9Fk15o59Dm/sz8eZ3oCTste18Q2mQH3DmHbHB/CBewsify0ufqejxFRP91DAR/bO0xPysOPH9sWR+6liiv/w4H9ODoRg4mA2HPFG4fZ/6L8ViWrzU5S8Y8r+HJ6Kw1xmn58St+GdP+N6u63d79QvPpTVh50K/4MKz4f3hz9cZ5ssYPvXiN1P4/0Q/f9/ohxBosPNfi37AH4Jn/89EP8FauWUdeD8qr0G9zmu892yJfich/VL2/fPR7z1Y+uu91X8kFpQEJQj+0u5/7fqqUI/KbxhoGP4nPQf2s8TC71b7i/GQamBgVRk++Y2PzKx3NbDasgwhF4dTWNTN06PVj3t3EGNRCkv+kJ7fSvKPz5KR18j+YrDejTheJ0cR+N+LjP+CCT5B/N+kindZ8A1/vOaAOopSP/ww9mHXfxjCyq2G39ygTKsfhXTyRfW9IB2lmA/MG6yT72QwyR8VVKDU/wi0iT8JbfS7q8L/DNvEH8iuW/+E4v/PgHM3XtBsjh+xDQv//7eI/l6AJQiW/Pyc9zsC9g0230Xwa8BOYeiWHxpgb27xY5FK0/SHP4VVgvhAvEUr8enQ745X4qc8K/5Cb2HYW/wiCAUfo7+K/Z5t6svA73cf4v8hxl+E0h9iHKf+FpEfSr8K5FDm1aypVydgLxO9vnHCfxz5/W63fkFBN1Xc7XbvEcsTINPyaSLbH6P/G/m6IIzc8SmM/AY9vJbvTzfkXkqRlxJ4Kfh4Hec+bmJi1oQxDJcwkc3AD4LjLmbuHI2Y47kLB7bAvxxoiojNuz3H5RK/nM19cjP3y9kSjRv4G9uSEVsS79uyET+kxH+ISx5KC+LKZhzLRl3IxsM6XPfp0cw7ZSVGcWE2TTgP6hoPStrzu7g+CTGiSQt3kVakPOweg2IiE/gL/x3Owg38S4C/zPBS9o2/n85RIjj4HL8/cIrP7+czf4i53f6yF/YXGexaOM7jONCkeQ9aCv794g9/ecg8d95x8Bp7bn8BPbEXuHh/BHvg4RI4i3s6Oz7v95ebtE/3y+Uugrbvr4dGSvrrcXcrj+t88W8HVhRAu3dn4pQyO+P6+AXj5ZQxnA1J7I81cUA/C6AWPPf6z3wRufi5vssejsa83++42/5jDT5Wl+MOoFWX5Hzbm4dCTGJHShheXXpfNkQB1CTTE8bU4SjG3PnpzAT2Awe7SF2M/SXZG/uDsTd2e3tmCe545Dg0pzhRVpfyAADBu7TmSTt7knt16KchmEW0AsU3TV/vSqoU/UUPnIkY8QqBEORPm56r531qIWom+nev3U6Yru/vGrKte0wcBI9hp4tFtF5drs5EJbwfHAXDqQlgi3yepUEk3Gr5gHPj4mA9sFHxFqqcPqraVgZTMt0bO8dioe7XDeIdmR7DOTOjFTTieM0Nn7Xv59S1V02wMvcKjhhJ4lQRQ7F03hVwkDhz9EKp4Jdovh6Iao3c/Wje8wgX7UeJ5a3a+lFTs0JT2jhHLoyEWw8UO16vEpNnFbML5Itw0Xm/HbwH4Gj+OtprI1s4a2xgS2wUpqdubqXrxwRsswUzrGgDDIqvNS/CTluyx6mTEm0mqEJ3VPGjmwnF1dQgKLnoJqvMqpzucfVg7AfZJZN7iYzHOrXSfRMq1beqEMnlZWdRM2zEvmuGIQBXv9BU5+aW93CKTJcfplV4C6JRmjS1u65JJO2CHuis04bmQV+IQK2JxDSxgAlIEUNOoxZkdOyxaXEz9olmE5cWtwngHfgiF9xU17KAPnfqxG/Xektm/eCJzn0hqsu5ygKJcYUBi+mddrB9xxW6XU3PtjAT2V2lbtJhuQ5bXSUzIPqsFBr5HqTr6d6jsnaRxQ6TLfWa9OSgES2438MoJMzdr2KzlRGRWSmbMBepH1Cqy/0btMhNLHuMEY/JwAXtOJ76k1I1pyHvLmCvOvsIlQoQwA6h1Ldx1/Btekj3h/SQ7MCPy6eNU0o6D4YYVWV37Xoms4gK52cQQvISa7ndIqvn2HuwhWRPhLuzBhK4oYMQ9uzqJufBFisXEmkFQaqAERBb0XEeS3SgT2Bjmh73o2uXZOajJ+ee35j9XdoIH1zdxCfZiTqf5Sbh2O34BwpLU0mfOBlfVWC2Ctg+9qLyKC5G6QCLHrfH7GOa7jIq4BqX3uPqqJWzLcqWvuxqZy+RhTYni6snUqcR1Y3vJ/QKTkgQuSTxKuUHzV4LzdX3bXSsmX4XYNHc4VdT3KTx0JvIPvNNBbPbVDlbcZr00UJphdfQSCIHD/1Gx+WtVKHtUKTsSsDyeJLgneR2HXPfnnxb8728uGPuFh47bWbviqsYtdv5kRHCxhCFctM3UrZx4SYx8WiI6CNI+p7u12PjBw3KT2dabG1VmnlnoLPhaJ+mVjuPahmPGsHl4CLcHfyYu8N4LLJziASjOCORz0a8ZZUNZmTqY+n40eV48mSa+FnG0rZ1HrhMZ4bjiLFq0nf+iIqTBaQJXznaHdiLGOuUpdf3bnfU0Y2NgcWLp4Ddb+49U6+GaYBDcYmtUPXA3gmgKMTdI8uX62ZrTqlBvtNH8POAyvNVsgiZTlYTxw7gKmnormzUeyuSzEeeH3fCI2Z2TScxpdOG9MRw+wz2TI1GN7XSb7VhDT5KtAi6y3xkUHdieC6ZrqeQ++UitN6x5QkNl9Ou7q/3bVXXTMUQBjjbGpdXNxoID+gH78R3PZFYWAjjnxSqnG5NDz1rty1pPkKsUzC1PxKeOG29z3mhuswHoq0J3hqbtmhUjKXPoVAjewzsQVnRO/fE8YCfu8PVQaIg8AgzEJdBXC/hA1dinxL2SZ+0sTo27HQLH0TinpXWwPFBXXAvlELyYg7d8WZrkvQUkfHMQDG+UQYJ+rhKOtIouqImoHv48XBQx3S6Kzc1f2BGMqIzzxPirVJOItlQoUF7uhfAPgsmk95dwhPLQKM9uSakqV5hCHqRCNfz0x297GxFbhbAzyL5kKN8JHBq3zhFqmLuuCrEg9lzMc4jceaKbHu0vmaH8bpv+8uiF0HrIlUZxFQ5U113v0HClerNDTqeppcoR/bg1kO+HsuSUe19xvKJPwq7xtZnsKMrfG8KSdKPmC5SzLvonFHF9U1USfzBK88ec8XdK8JJOXM/kkql1W5TEpqebtDv35R4FM2740n4uPSjrWE7cgy9lOM98jJI0DNSPDp3EXkT8b7eXVtxIOswxHE7v+rWFkkMK/UeOMxFInq816BG/ENbi2yTKFs2SXyR2aTfxa7miv120gpxqRwEKftx01gXGr7JCT2CSX1LnlxkLWbBvUnbVIuGQ8yn8dgGU3Srx7BrreUcHBjMRfZAHIghG+wamYiJglY1aIjax6FHwoYRdVEmshnVipxRE7/t0vEwtkfbnDAqYZQAah6epci9fU+KHRXe+6vqZY1GeGZ4sXAf8Tgxw0eN6YY1mI8sHJJ5J6nTANEIe91D2ECpT5ImAjmEqK6up9dBF0+71BEkTI17tAZOT993dNsWvbdj6RjtDD/v5Ko6dwN2sTphd0iirJyCC3YgzRtzPPiwFT6QOniGYkJ7v/rR2uTV0ZGusjhKyTa1B8znyXY6laflPkWreVQck4yJlt7IMMMPu6gNeZKTUXveFIR2geyBwky8DeKVPQKlzU+wLSeortoCY3lGMVpZ0aqojiqjIhM/PEwkdk+S0mX4zQaeuD0cDvpNkUTsYaf9oNHAS3shQlkjmdEUkprzoGNyWS7YHFMEqdj0KXfujsJx20W4N4GknwcZeNLHmVuWwsQN3B7YJsF8Y/KW21yXegQDhIc/d7LqnDe/vxqWp1x5bYkw5DFIDAXUX9QxQkPhxKnvnCwXsGS75+tZkcXGKpwDAUXRI93sijW1OWBmDI7TQmRpt++Z9TKnjXfGHuomuZUD9mSaGzw4lM5GRLmguVVk1iobMyu2VrBVZymfTtZZ412Ci9OFx2IKLbEbdqVbBwtXMj0isjvTXNRH46Ddo7XsBB7zG69a8lK7jzHuErY0Gb6lp8ZgD2Xuo6xEkgErku4BG707ZkWhhd5LRdzuN6TQT946SJFEAzyIZyC8yM4mGTaCfkGmz8RdubaKlC3bPMkiI7gS4GfcQR40mjQKiPYy7+FdggOrZYeG8IT7SsUSTloui+N3P3Eftis5IMRCd2WGJCf8uvITlrJPek47luZhHvUwG0pw80tgx16WXzq3n9ZjtZ2GGNOFUgZHqLVVn1zsjDjXq8beLFi5Yn8ibUBozd38LHjKKNXpvY5qLNeV7TWDUQjHSzLbq0fvxH5EqT3IAxMbIGjZbF0YWO1xMlwF23tpaBLntBzDtU29LtpzCg35HAomMhr1R43tdIY7IkuPjqlBoGNAzxNVNb3Nx6r/OLSp0fTE8FgmdfE9lkOqelKO9O56W8mxyUKGxciRLiNcW+wiRnA9ik9ydoGVlFiDJxHAZqnLtFVfPTo155PQRdEL4dosWiExdUUPMtOOJumK9FWae7EUMxPxynw3MyKqnIvQnpR02B3jx6zCYMP0XBPoHKs/I0jesORyIOHEc/6yQ+iTWdNaUQbDtuO3ADeo7oh6DwnXAkgxd9mzHyoBQnRMzIVkdPbOqrEHT4hnCS+9J63CZw97r0p9zvmrx3tH87JTs8MCHIoqXyw7GCIj9gZbzYCODnp77NF1XurNlNQGXNXM1Kn1VV4/uAlB3/2+denedYBE1w6bxOCuwvI+Kxau/ahbNAx3jnc83R7aSWlnWmgc3MmvJkFfW46sHqV0ExrSWlR01CZfdfSLcu+1WroYqXa2OMYMGo1pU8TK5uTyKK7zJTyGvng/XQ9nC1TmXqrsxrM4xAAfMuMlv4/0iZHJGJ2ZWPT3VujdleEOwzHEpjrFf6xO1cz2nqd5egsqVLB049JifXIvMKPnq3MylfIVeyCChlFRQkTepX107ukGdJ6IXlfsjKJa22LmKKECaa0zO6taQbJVqoRsFFDKTSQNg68FK+Y4i+N6TuPstjmS2YkaTqdSOhhDDb240SNeXbnVDj/A8ERNR3nnV49TuLi4FjEcFCVmWM6BeK6LhI5VoKTPipSjhwkElH7XiPNju6ShyBgs4QqjrypXIlYmI4xzqbYT7lDfC014DEdjzfeyOrnmmVH5YfRFp02rLGvc1t1nfYOpCuDK49ElLMxdasw1elspquQRVej+gvRBacI8DLcbDgGhQRyWJIxn9ndoDgoqalfcPm8YI6sHJUG5CZQWVu2asakuY7fJTTFa/mnJlyq6MAJF0ihgfJjASYmog5RrdbjlkfytjG4k2F7VRr+xTy458Pl2RLKAyftMi0Mfh51iNeIwBeSNQq+d7G/bkS1Oh/QkfpZKU2rfQuOurkLpKbxy1ZHY9aITY+klvO+5K0FcuFpXR3/ASNgCES34Z6MV2M7TtdaTjjqNW68QePzAQ6oArs48H+eJLcgNoTEtuFpAXIvDRQMcOQzb1rF3t78p7i6jRdgt94LfUu1ALyQXIDDERo/6PEwkgfJDqSYCTZ9hU2CegKmP93NL5WhhCX3IdlLmb1qvn0j93OY684gr7uZHuoPc6DLcC9d9GTNT4XoJYzmihBOWKscIH5wO/eDeT6fJCfTB1CSACxj5gSqXKVIpCxyWnRyzVefRnVp0FgzllPReWtAPXuNGOFK61j4MaOdS9RTC94NvmaSqDU8BCcNhwoYoVqF70ygXdKH3NiaD0KSFvHJgJj9xJp/EmRpmccZT3Y44kfXIRSMmOSsrWrwVd+sKTcePbq2FRmsVdw30V+pJWdfKOqMxxZI42067piJ8QDWlMwjlSO33EdaSnT4YeqbR652FficFY30K9Kcc0VcJI7hxP6NEsuysI2zGNSmne+XUrsIIj2y+O2s8A2YFmscPUXB/ZwVxzYHMNFY8RqnCuy5s/XJWws6GJD/tKAfWO6qlylb3ZK2nUs5alUYm7s0RLvAaBsez3L1aBVMpnbSR28tYHcvzbYBCPMAHnY8QKYmpCmZ+ShueInvp46g89awK/NMF9QH7ghA2ayw35kj84ErCeqR4UyZCA2vwKjpEx+5SrPez0ggpkRV5ea6PlzlKdx1wpvX98cmjKsdz3V77tryXm64ka4AnFEYp1lE1SPE2ndxUvF8XcMc8ibdYTzDWgn1ajfcgj2p9mlVcVpDb0DqNOi4A9eI6lba4+gp81Y0n4iPpRpR+NwDONPmuuzL2KLUw2s2sGaYZ6EzosHOn33cwDUEdJUigT3m4wTL3soMEhhaFAh2ZCwTMqEz83eSMZeI2vbtit23nyMNxlRsbaobA61gQfVX3oDGXPTWfZwarVJYDWD6HOFBN1dFVbPd6Cdqk76Ao5vNSImuY/ThdaGS3Lp6hAhr0EariYiZVFIkquWXyZwoblevG98NuHkflLngWnqy3Hnuw0IkmPorHl6gfiszfcUZ7GDXDUhSTyWAuZXCdzgxZERdWua9mJNil+LlFNCykFDjuiRLS+YrBAAdpzIt6dqCcjkc5NI6UoorDOeMuFwP4qQMed0cmnHsE2eqNPiK9xk50dgtNpj8XyyM6H4Y8ICLo5EXKUYnmibhcSFxzCJMD1HWg9hzsYFRTCR9cF14YaDxzCa4YNN8bU6hOllRLJEQmTl3ZyTgP95K5Wl5GdekGYNLSvOunyUk/uUUOA56TVMEIY6w2JORtQ8mKM2KfmciY/YizT9tVYqSEJ5XLPjNPcVY2qq8/WfQ1pe3eZ4LmIaskrYUYNq4tdibddPGuMf6UEBs8CnpxOFo7oITzSjse/WvIsiob0DR6Fx+4Jtq4J6qKiw2qhHcucufXYB0BXtNqZPaxGvLBjHH7kN1QBo0KXdlTZ8RvD8A377qbEd762N0R5cwdj9osmLe4dNnaX56ScbudfjxdRap+Dq9PnRPMO7NSki4NkSqWXX3c1fph120OblSY3U+o18i5ffJm93G/dJKMkzuePXolJIz6rswLmYXsgojd8RLyimxA13kjFDNzLBUxFlGr2/ooC4u1Cjo22lUKTKhpjld6Sw1Wa6acG9TVacK1eZTVxcc1XcVhnood09oyR6GsS3GBPnhdogVXn/KgaG3JaF1EqneXj1h0U90rmWf16D2I297zJ2bH9tn1NgUVdj8cUkW/ry8JBYX0lThxyLzLZNIyUP3snZVUqXA5xmCTCnXQs8lKO60fmfPZxz0uWK/2HPINc4Ee5NT2Y0qUKhb1TXdtVC7EFdwmCy+OSL+0DlFjujueTo78wB4P4iT4zXV/5OXkelGlcIG5glLzoVwnDewa6qoSrYPRpP4Z7TJ6ALsDXfCDqsGdx61J6/rR4duRw6C+5zsGWMRaa0XTZ1EqZINk97lgILbulGc1sZ9iIX8gHCimhGO0uDp2GtraJqKtmMh0npxO5jzaYWftNLIqzO+gtBB0iS/HjXc7maNeoLumXzoJCXAxXLdO2YR+fVy5ALu5Uecq8iJmyAm5GxgA3SkVEQfZU1blE27GjsaJjAtGJY7zZt+YKD3XfX4b8g3mv0j3oem9RnfLDEJhNTpR2ZGwz76liZV65CUiiXaYrQrJhRAdwRab9nwfFDmO9xJxGHdqfYuycN+3D2bB4sxEMRTyqBeKtz4/N9o68OeQpLOMrqA7VhbMf9LBY++aKqNQfjoEGbYO7KWPF11wTjC0cw4YRmQ3wpvvS/eg1tw5IX3uyqo2w2v74am1DuQupoDfiiABmlqWAEu4B6JnjdLkh2kKpFoMIsH70bg+5ZTv1lPiWvat0WPX3ZSUJmtFxFFWgXJHzWMN8yR8ssEcQwK5A9CU0LGPOY+5fU5xfHPUeatNV7mEbJ5O7uyVLAHD/xNKXOOzqjcPjB463LWBbvYa55gPvme0ZYMfkaAICRznDckMr7pjlaVMVeOAkhWgZ1GlTlhdCyfvxA9nXPUcpJGSaWZsX5NQp1HIija7Wbr0ONAn9JJu0boJTfCYht7rhNBBmdFtWCHd90pPHclHMAwGCTi7ZY6Nrfm2dlRSL69cE+g15nYAen3BOiUvFJyypMXeuHO3ndj+nEeR7/Gbat/km3Y4jbumvIQC2QHMSsXcb1TQjWPt88t9s+7uZTb3ZxAJanZR2PUYDsV2wCeGSx4kHu/uyIlfxlFow1YIDo+5Pz6lORbZJKZFHdLbMuaX/bLUALmwx2m93EaH8XS14dkd6Q9RspEu5e4OIcLk1QNtDqEZihuB2Vt+g1L2nJFArVC77gjIa03oO9FTdeXbnFfiPVS0nOAMMA9fFidiKLX0lMcBw/WQ8utgp54IrohKBWUCnjscIsaYqRQmK+lRo4txxzJx6w8gHgdl4GIHEr9YT7nCu3EKT2ZbCl6B5Nlw1MvrzLFidz9eh+yBqRMQwFKW885x4PZXdTeQ0/FaC0TGngpOSXBSwY397dR/0oji8miuy2po3kkjjIZwA4A4qOv5oGWaOiYbl7CF+GJMPDC7iyu1zKE9KSffm0MTNGFEBr2fME3GNEaZkoXbEI8KyPHpYSO2py0VeJoZyZnR7EXeOkfVTbUfk+ujDbTkvTzm8oNCx0i9lGfyvCLbiCYjm139/Y6Gauym1rAXDhYQeVTm6OPCr3pBduo5zLrzPB826KJhskMUFrZN6IBxmArytt9Ns7KfVtZWTE2ngNNyqnU8jvx6OFAMyjun+8GxjDxAu9TCpnaLPWHpJxtw/7nF5XulPphQ8qjhwsbB2ZK5gd2robXw86q3g2oY4XWIMFZbYSSIutvUwrQvr01tYzhebOVxZ7oX43ZqHOfBYzUdHqJWlUNXj1wV34RNNm6892ijwsgHLDkj0DxEiyxrjRhwjfDCcKRWrDJQmXBEAys3yurxAi9bCbIcd26Pp3CTo9bwuicNYQY41D5mbA+HI8/zUhxTO2GBWUKx1h8oURP3XfxgN+HeTNb4cFJGvYCoiNfQBtHk8+linVT/4sXYAcsuTZquwFGzXL4lo8/RG9ttFnTDc8UI231THvomH90yrfeWnddsKyakuEUWdMe4E+rYRaUmCLz1ysqAGxocvVl4fY9EAhUvmNr47Owinr3nQncbu3VrK1cq8ggZxoi9ozx6t48nnzLoqiLTK+HlS5H40ahqExvcBnZw9UEM01HSHFcw11Lcnp4eLzMiUJcgOsN8+l0pciV9oSDTDE9QAVqJvF/PNxd4wxMFAo60OhfMTjWfksiKoeqhvsiEASU+TET1lV/i6FHAbUdkLBTnJ5oqGlEBgHOlsysNrsLxV6tE+RJYDXIXK4jy6uk5G3t2OY5TOJfj+x27NxwXKTjHesBw00p7mH1v9VFiVHosIBqSC3vbegb49mmz9/RKXUA4ZvvN+aIaNz1UlYu/VEh20fe4frdspZmzpj7wkxwvpUfliuGJ9U4dHfU6g76Jz2MBgs/4JGRMy69Otefz1obm4PSblRENRM6qc9qROKlFnlfFUcVXPejRnI7Vleq1+thmapZ6uFWSRzbfA7UmRw/OwoXy7h/asJpXRQ9w0lSg4TEEXWa4RW8EDYW/aXSyveuy4DpFYd4/7EFkpUyf/Ck62ryEoAy1lD4iopPvmh7nr0IDPO9paaU1AA69dGHYuQcBlcEonvVAfBAh3lrHss+mneeole05ieNugMqU61fBr3hnemZ3pkM0k6mMb+RlQDEys7QWploExzZnzDlo6EOVcszXRjyPRPfSVoWSt0fTeXi7q0WOtU5kPr3qZUscrX0oK0kepEWZ+QKRh1rI+gjLm+hEFerhAPHoDp46Gg0y4DhLTuUkKG4RUicpoD1dpvxYym1sHym+hKP8/QSUsNejswJkRY3zPHT8F/Ka92fxJih7wlKmyIMa18WmaLsbaTgn2/UmeIAYYSxC3ebCJcQuuVSFHZ4LlOTRLcF6xXMW2T3z6D6dfU1cYMZ0N4zio5NvHDmCazi0HCLA28AgjNpKbqypVLvI2gNIvpKqei3GCAxIr7l11i4N+PPax8HD5GRN6rRhwJPNk3Cu8RHFXm1Z5oAkEHD0emjsswM9o3bA5V0VUBN1EtZgYYsbhiV9ZuBlFIxMCHBx4jcToY7LvbP5uRV3BJ6zwo2yBTXqQ9xG5Ym/2WJfuumOVHy2Ws5jtHcXyr+36RG5N5EcpGoV7u6Kg5k2IXt42l8Nd7EJ4bq59qJuGb2FFDgHPqxJTtXBhGywnNgJ5TRWOaHFkGnqpcYt7fKw2+TQwS49aLYDQuqZiMYpGS85ht2flLmOXaP2MWE7m9Wh2m/b9ERfHD6SdcTF8PLKFgoSmndFbrYdQ+PQaeWKsNpC5+zPytMDM9pzjkLm9wyUkELjbO4xCRap9teAu2+zflgyRa8OIU3w9IGEfE35qIsZSQ2og1MvXM8jO1aUFU+eRi8A3usuOpI6qSTlF8zgeRdIX3xuqxbqHUrJNAC1umt08bb5Fu/ao92oZzQzyUWwYvuoxdxpIrKsoCzXiTdLYnxHiyf/WhazNAtXz8Ov7d1d5H3jXypnh9GPlR4HwrM1PN+RhFKvaMsJorEI4xK7fEMvYgloIkJB35Y12dkP6LMv8lrjWNrZmhcryaSzJ3n16hFrH5jHEVrAKoFOC23npeV+KMrgdr/Oq1gFj5VNLyqQ7+ToCddHdqu1fOEHbtksTm9hLgQLvMfSezXbYAiG2ORJ6FrMdLDowEy67KxSLeRtdY7go36Tr083fsMRBgU+LnPq/K5fM9ITPRfOBeE1dXJu+YOTYu3QI35JbZs6thlT+ee8c0a8PPpLgxT4lLtllkzDinXMGq5WXh2iw25V7BFaTzHK8mwlCCI8sCSTks5KZw47hUBfHJQH428Zeq0xvdCKY5LrlN2nh1r03059mQpiVDyUBWVteFwvJBF31tOTIM3WXczHUwMoMi8ocSPw/dK4+fFWn1L62o4ciOjUA5wVYMAMiRYkWe4FIDIJCG3udOa++u3pVjCOT1ZLP8pcdOYbKjPWO3CZuBrUlZNHyqMmy9ukBAfP3+lFiIymaKGNi/QhUrIx7U2czz+Asmp3196+BbbEdjCfnKwg9qkszlRiNzgwokX0UVtSDak120R1XeMU9h3mw9SohHPUQHvu0GS1R5o0w8Z3w40+HmIXaMzED+7X6y1HMxCclbQt3IxJYmAGjVNEGz9uwFlduIUUjkbn9cECM6/YAicf1WtVFF65ndPCYzZzpxnu5RKaKWmf9f4coijLbT0RqMCKEuZYuBxj0waO2NnR2Lu9is1sVQ6FcfO4dtt3XFA6UsTaI3D/0qKXnSfeRsGkHudsYNiFACq7z42He+eZjH50hnOSM44xMBE7tDD9Tdq3TuVCuyJ5MDAJ2SUfH+H5/bHHdgYuGNEsohS2rRXf2EmVySRvkSJwtQ9jN+dpmU2DjxsUzhTsRj5ucy+JTKItGezvwAlDHpJLQ1MHxtVd+OC27c1TAyIXvhpuSGdMgTO1k1mteFOFiNwTRQ0BgeTVajc9Mpyoky5n3dW814avyuntNh/T3GUwlMP0m7sQp8MS9t0sYDeBA5H9UUD4gRJ99WG6jzDA27HSOdF/CoKRG8z1G/N5I2W+xrw1mCUEiPRgac02akaJY5eYUaqQ6ObOXqWr1oaoMpV0QwqaGGsXezCI8XANTyIIlDx61WTlBkgPqT2dCHv+xmryCoWVFS3qrYypdn6ol7EmstMD11uHd/IHMCac7NyBJDkPlWZHQzdcugybrY0muYFzgrAlZSNUmM6FUnvhtCvMivjKHBXHe2VrdFNmgGg9WQ0Dq2G2mSkIkrnPaCussnNDr84ORufcIQf6ibzYodNPQa8K5p6vVUMPN6CMtspT7cBrqHa8Ifc7CCP4PMeLrRJrMi/nOwnIw3eugqZWax6ivTd7T1EeD1tn2mcCVi2PbjQfbFtdGqowEdjtya0UBXprLudQwovDhrsm33qjq3E4ffW73hCrPWrFPFBAIvH1g4CvNmC0ciJ04bAj5nNI24xQ+dUOAyrbRe3a12aU7G0t9YVHn/vWXvZ9mkXDu/hY4psKJ0Ly2DZkGCUL7DKfIlGtqebI4TJMR53EY552IbeDsd0KU74yfPggYJQHLCE4mDrVKXWQBuJVGMfUmzPu+DElLgb3cFoP1cNbiqd5pUVAyAbZqtr1brc0j6c4i5qHGsfHvIPCyISaax9u9IMK6EJ7NMVM4B63qfIyIVWN37QBHtWijrWalZaHJHyu9qDGCyrUTZuvagsncyDSkpGVrccwaMfuaX2uTm7uYX3XMwNSbKxgMSUMLvdNXMSJ1IQBSpLEMUo2Ex9HCT4H8Avb51crUl3TYnxr8smZsFEhvR/v4W2vnrWlJrAxAAHmzojuWxgp5ShIflB0dirtNWHn8TUKZ+kKSYu3G6mRWt/EstZTt8ykH3iMs6zDFMuJipFmFAy6Gw82dbZFLOKNgc9RRTMUa9U0xMBZz5UYqPpO9NlcL0mozHgSqgavdowREmS23UvDLBHz3gpX/6yZzV443Q7dpOnqTVNkNy0CTH1sgyS73j735uC6bEhHd0x1cIvKQ7ykQXuUq3xIPjMdB/LRovenHXA3zgHX+Bd3Q2F5NWMPPfPNZUE0TJNn2Fsiw7FZYuwZXWvakRIPhWvtMhb42fNpl2cR7NAtJjOsSne7Bt8xHZWe5tA44T6NjdmEj51ibjfs0NNAV7vqYXK2EMRiMDvSJ5chytUTft2x3TIGuB310VFjQpiIaU4nOaLF4/6cqe4BlWj8xhrk7dbPPTs/rp13UOXsSRA0/qk+BponbA8G5hGaw/0Y5cuT99rRtVUhJ5byjsA/Bwo3e/go10qhnPdXD1tvxNNzYvG8R7KuOnfz0xPzwY5HqUfGnCN2JsnwfofPNk9ZF5Lx+XO4MM1HUW/l2BYeZSOy9o58WfZarrujNKebgWBwJmV83OfY1dY0psKuNdFRRQ0j1AMxIxaIMzHOgkGm2RbfRv/bDcO6SULaZtuOGA4qlrvAUIbdYbJG5doIYqi58i6xlXyJD01pA3GmsTWdMAcinOKG3dsdLi5KOkm9wNfgiuI6QhmKZEdLezC2S1iH57TpIR2UOblv3LnQ5vLcVTnZHB5SnhI4TDh5t+6RKgOTNcJ9JRwJjWsUXG4ekW82QnxyqCKbw/c6vs+6mjjyAaWRz39evdSMkuw7r0/R7AeKfOdtR/wHvT1Fvl2w7f/w61R/9m3o7/861X/0yiTx9nVoq+4K4DkRz63y/4a3lpJhgEuKcx9hOM/zhxm2AFb/Q91BLw6XzX16u1sM4OwtcYYLbgCiBuP8awHMDW7UfuoWvz6Vfcia+DstfoThHzD294BMf8DfAJlFP7y7Rtt3eA3yECyTll9dNq5DfbO9MAjtX1+WSPjCBvYlcMHw3diwm1L/n2VI/lmG5H9qGZI+LZsi/C2EVv5b/2zjP+z1Z2AKX8Oewah3lkAj31n/7MctVUD/DF/9rfXPPu37y9Y/+72VSv76tbD+s6H8Kcuf/bze/5stPvfGd3I6iDUQyR3C2V3/8Zz/RZ7zn0XK/9Bzuk36W/xs2t9pHR/6a+eIMW9X7PpLXSOG/2/w6TsrO/7uYoV/E0J9u3Ti81cfjLApADAG+ErNNwOTdyngRwPwHTzhMAjNnw58vd45y9EE+g4BPeckvgOmKOIVphj8PcFJvbMOHvWjskU48VMF549cpPXfh+h7C239N2D05bs//+XRw1+aNnw/kkD+pOj9m6zChFOvPjDw01dh+t1ufe9rbp8/5vaxAA78L/AzhC9jT7Vj/fGAFyv4ougLk38phBf49eOHtThwAOjE5ePlP5308YZn4/5yT9Coj7f9uirw+cM3vzX3NXz+4ON938i6vqxM968S2b/wBYrX6y2jJP5OivPlsxRf5Td/1BKsxM91PX8lW31fV/S72eK/3hW9/5UIhnmDdb1LJxCjgEI1HKAO+xOfcvwndvzhsePU+N+mpu/82Yk/9WnC975M+D1Y6N0PtqLoG5v7C1joz8uXV3j+N/XMWxIx70FxqzbZkIyWvlWKLeParz9Vvb5dJVb9vETsP4Hl7342jvoaVTj7FlV/aVj5lvw5MI4r6Of+n8H8F7MEP30wceznCrUfmCP46ku//4FK+y99RPX2KcnO7TyAKAzZdWGQwlDol6evAuP8zi38sXjO8wlhE1aBW/nvPEn5u09FKUHD3A9D09Vx2H3o4PSysSlqN4Bza2GgCCedwtdnqzoIf8v631Igmn714dcIfn36te7SOAWm/KGpvteEFPyVLCKZt8EZ9U5wRv0o08B/iiz60TD9s7PB/mYwfTsZDHjQ9EkXqWPpwWXyP8FUCiu4tP4/KP0LUEq8zLX8WSj9wy8r/KO0fn8K0N9MaZE/RWn9PXLuL6n0/5JP3hH0q5gLeyaDH5pBf+mkf+a1fHGHf+a1/M/mJr/7vJY33739K+e1vJuA/CnTWv7dpyDvUu8f8v23c41f0/27/YP9RdL79yr5Bd0qTx+IB2UHiIzIfW8W/j/a+t9IYZJf4/KvjIDftbuf8nTy38PXt3HzJ+awsH8Rvt6t5Ht5Ynh3vqj93E/ctHozCm+s/ovu+1rgCO85shdQvvZiL0hJy6cXjNOnFbW2sQthC70vqiNysPS3zzX87TnI+tBPn8HwxvLfGcTfmXyJfQ0GiiHfmSj2kij+6jkZ/YNG6p23kQCVDTFw3MLbx7Z/ewr8yHcf5jRPP7Lhx1fS4HYDt59eTyvLunqiRPgXrrH+0uK2+C0swiZxq+H7jTpBEB9eAvrP39Fm3xl4nP1A4++M/b+OUrDZwY8ufhGkQPF1BrwPj/h/7Vldb5swFP01SO1DImw+85hkaTetlapl2p4dcMErYGZMkvbXzyYmwEzUZkvSqEqqqHD8gTnnnusLMaxpur5lKI/vaYgTA5rh2rA+GRAOoOuKfxJ53iBwZDsbJGIk3GCgAebkBSvQVGhJQlx0OnJKE07yLhjQLMMB72CIMbrqdnukSfeqOYqwBswDlOjoTxLyeIP6jtngnzGJ4vrKwFQtKao7K6CIUUhXLciaGdaUUco3R+l6ihPJXs3LZtzNjtbtwhjO+FsGPJPyl+243Lwt+I97j6+L6deBmmWJklLdsFosf64ZiBgtc8OaPNKMT2lCWQVbvin/BC6asxDLiwBxVnBGn7ZUWQLp6IMW9bSKlSVmHK933hLYEiVCDNMUc/YsutQDfF9No8JrAICtkFWjlldjcUsp31IgUhESbadvSBQHisc9OLU0Th8YjRhKUyzgOxxGmGksNyxKTlcx4Xieo0C2roS1BBbzNOmQ3BLDNC3x1eiXfSXBRMTzOCFRJjBO860or5C/O2J2KjLq6uHoYgDQI4brHksMRxNjyjDiWBOg5ukOLXDyQAvCCZV8LSjnNO3y388pUmeBIFAo/LdMGc2wtBFJko5yk9kNlJ1jlMulpOtIptIheikZHoreclBOiZxzthRTF71OA0cUFUAwhE5HWBfoytre0NGldY6lrKsrS9McZXLh4yAQfuItu7mJWM9kIY8ieXT1naGseJRN5hxn4fVHdSS0z82SnibcuOQxZaS4uHIvV8Jh15TO+5sS6gVF40q4tzO/4QCTJf6w5rTBuZkT9GyYUjDWn1Y/piyOe26y1HV5S5Y5yaJEVpSVX1BQZcbdgoDXBWknQZUXQ1TEO2p72CfhqPqc3kddvSxLF8z1ewQDR0uEvqbXl6IopVxXBhQ8mKIMDQnXM9tlx9unDrW8ni3PH3qn3PNGmtT1DiacA81JQoMnQz7vajvd4BIHB4oD4FnvXfqAvgcStXM2yrckdn+XtIIpCzEbBBsRxtUU7GowaOPXFZ31iDqAmv34klT+ea8fvS2pnDiY9KTS1NEPjKSougc9p1xC4gCPzN45hgTU3yteSsCdJWDfe+ATl4DQ/n8PXyqEQzn4DCoE2PdsXQeEetXR80LkktIPkB3s0waEOG1+2avaWj+QWrM/ \ No newline at end of file +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 \ No newline at end of file From 05a8703c2e866169eca2216ed9d47d0085a5fed6 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 15:34:16 +0530 Subject: [PATCH 14/68] added error msgs in create user service by localization implementaion --- lambda/services/src/i18n/en/addUser.json | 7 +- .../src/national-api/user.controller.ts | 170 +++++++++++------- .../services/src/shared/user/user.service.ts | 79 +++++--- .../src/shared/util/helpers.service.ts | 78 ++++++-- web/src/App.tsx | 2 +- 5 files changed, 220 insertions(+), 116 deletions(-) diff --git a/lambda/services/src/i18n/en/addUser.json b/lambda/services/src/i18n/en/addUser.json index 57af43ffe..d91909566 100644 --- a/lambda/services/src/i18n/en/addUser.json +++ b/lambda/services/src/i18n/en/addUser.json @@ -6,5 +6,10 @@ "addUserToUnRegisteredCompany": "The programme ID is invalid", "createManagerOrViewerAsInitialUserOfACompany": "The initial user must be an admin", "createUserUnAuthorized": "This action is unauthorised", - "createUserToOtherCompaniesUnAuth": "This action is unauthorised" + "createUserToOtherCompaniesUnAuth": "This action is unauthorised", + "companyCreateUserShouldbeAdmin": "Company create user should be an Admin user", + "governmentUserAlreadyExist": "Government already exist for the country code {}", + "companyCreateNotPermittedForTheCompanyRole": "Company create does not permitted for your company role", + "companyUpdateFailed": "Company update failed. Please try again", + "propValueAlreadyExist": "{} already exist" } \ No newline at end of file diff --git a/lambda/services/src/national-api/user.controller.ts b/lambda/services/src/national-api/user.controller.ts index 54508ca4e..8d158f533 100644 --- a/lambda/services/src/national-api/user.controller.ts +++ b/lambda/services/src/national-api/user.controller.ts @@ -1,80 +1,112 @@ -import { Controller, Get, UseGuards, Request, Post, Body, Query, Req, HttpException, HttpStatus, Delete, Put } from '@nestjs/common'; -import { Action } from '../shared/casl/action.enum'; -import { AppAbility, CaslAbilityFactory } from '../shared/casl/casl-ability.factory'; -import { CheckPolicies } from '../shared/casl/policy.decorator'; -import { PoliciesGuard, PoliciesGuardEx } from '../shared/casl/policy.guard'; -import { User } from '../shared/entities/user.entity'; -import { UserDto } from '../shared/dto/user.dto'; -import { UserService } from '../shared/user/user.service'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; -import { QueryDto } from '../shared/dto/query.dto'; -import { UserUpdateDto } from '../shared/dto/user.update.dto'; -import { PasswordUpdateDto } from '../shared/dto/password.update.dto'; -import { Role } from '../shared/casl/role.enum'; -import { JwtAuthGuard } from '../shared/auth/guards/jwt-auth.guard'; +import { + Controller, + Get, + UseGuards, + Request, + Post, + Body, + Query, + Req, + HttpException, + HttpStatus, + Delete, + Put, +} from "@nestjs/common"; +import { I18n, I18nContext } from "nestjs-i18n"; +import { Action } from "../shared/casl/action.enum"; +import { + AppAbility, + CaslAbilityFactory, +} from "../shared/casl/casl-ability.factory"; +import { CheckPolicies } from "../shared/casl/policy.decorator"; +import { PoliciesGuard, PoliciesGuardEx } from "../shared/casl/policy.guard"; +import { User } from "../shared/entities/user.entity"; +import { UserDto } from "../shared/dto/user.dto"; +import { UserService } from "../shared/user/user.service"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { QueryDto } from "../shared/dto/query.dto"; +import { UserUpdateDto } from "../shared/dto/user.update.dto"; +import { PasswordUpdateDto } from "../shared/dto/password.update.dto"; +import { Role } from "../shared/casl/role.enum"; +import { JwtAuthGuard } from "../shared/auth/guards/jwt-auth.guard"; -@ApiTags('User') +@ApiTags("User") @ApiBearerAuth() -@Controller('user') +@Controller("user") export class UserController { + constructor( + private readonly userService: UserService, + private caslAbilityFactory: CaslAbilityFactory + ) {} - constructor(private readonly userService: UserService, private caslAbilityFactory: CaslAbilityFactory) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('profile') - async getProfile(@Request() req) { - return await this.userService.getUserProfileDetails(req.user.id); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuard) - @CheckPolicies((ability, body) => ability.can(Action.Create, Object.assign(new User(), body))) - @Post('add') - addUser(@Body()user: UserDto, @Request() req) { - if (user.role == Role.Root) { - throw new HttpException("Forbidden", HttpStatus.FORBIDDEN) - } - return this.userService.create(user, req.user.companyId, req.user.companyRole) - } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get("profile") + async getProfile(@Request() req) { + return await this.userService.getUserProfileDetails(req.user.id); + } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User)) - // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) - @Put('update') - updateUser(@Body()user: UserUpdateDto, @Request() req) { - return this.userService.update(user, req.abilityCondition) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuard) + @CheckPolicies((ability, body) => + ability.can(Action.Create, Object.assign(new User(), body)) + ) + @Post("add") + addUser(@Body() user: UserDto, @Request() req, @I18n() i18n: I18nContext) { + if (user.role == Role.Root) { + throw new HttpException( + i18n.t("addUser.rootCreatesRoot"), + HttpStatus.FORBIDDEN + ); } + return this.userService.create( + user, + req.user.companyId, + req.user.companyRole, + i18n + ); + } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) - // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) - @Put('resetPassword') - resetPassword(@Body()reset: PasswordUpdateDto, @Request() req) { - return this.userService.resetPassword(req.user.id, reset, req.abilityCondition) - } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User)) + // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) + @Put("update") + updateUser(@Body() user: UserUpdateDto, @Request() req) { + return this.userService.update(user, req.abilityCondition); + } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) - // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) - @Put('regenerateApiKey') - resetApiKey(@Query('email') email: string, @Request() req) { - return this.userService.regenerateApiKey(email, req.abilityCondition) - } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) + // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) + @Put("resetPassword") + resetPassword(@Body() reset: PasswordUpdateDto, @Request() req) { + return this.userService.resetPassword( + req.user.id, + reset, + req.abilityCondition + ); + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) + // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) + @Put("regenerateApiKey") + resetApiKey(@Query("email") email: string, @Request() req) { + return this.userService.regenerateApiKey(email, req.abilityCondition); + } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Read, User, true)) - @Post('query') - queryUser(@Body()query: QueryDto, @Request() req) { - console.log(req.abilityCondition) - return this.userService.query(query, req.abilityCondition) - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Delete, User)) - @Delete('delete') - deleteUser(@Query('email') email: string, @Request() req) { - return this.userService.delete(email, req.abilityCondition) - } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Read, User, true)) + @Post("query") + queryUser(@Body() query: QueryDto, @Request() req) { + console.log(req.abilityCondition); + return this.userService.query(query, req.abilityCondition); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Delete, User)) + @Delete("delete") + deleteUser(@Query("email") email: string, @Request() req) { + return this.userService.delete(email, req.abilityCondition); + } } diff --git a/lambda/services/src/shared/user/user.service.ts b/lambda/services/src/shared/user/user.service.ts index 740447141..cc9bdff96 100644 --- a/lambda/services/src/shared/user/user.service.ts +++ b/lambda/services/src/shared/user/user.service.ts @@ -12,6 +12,7 @@ import { InjectEntityManager, InjectRepository, } from "@nestjs/typeorm"; +import { I18n, I18nContext } from "nestjs-i18n"; import { UserDto } from "../../shared/dto/user.dto"; import { Connection, @@ -105,11 +106,13 @@ export class UserService { async getUserProfileDetails(id: number) { const userProfileDetails = await this.findById(id); - const organisationDetails = await this.companyService.findByCompanyId(userProfileDetails.companyId); - return{ + const organisationDetails = await this.companyService.findByCompanyId( + userProfileDetails.companyId + ); + return { user: userProfileDetails, - Organisation: organisationDetails - } + Organisation: organisationDetails, + }; } async findById(id: number): Promise { @@ -188,7 +191,10 @@ export class UserService { .addSelect(["User.password"]) .getOne(); if (!user || user.password != passwordResetDto.oldPassword) { - throw new HttpException("Old Password is incorrect", HttpStatus.UNAUTHORIZED); + throw new HttpException( + "Old Password is incorrect", + HttpStatus.UNAUTHORIZED + ); } const result = await this.userRepo .update( @@ -272,23 +278,27 @@ export class UserService { async create( userDto: UserDto, companyId: number, - companyRole: CompanyRole + companyRole: CompanyRole, + i18n?: I18nContext ): Promise { this.logger.verbose(`User create received ${userDto.email} ${companyId}`); const user = await this.findOne(userDto.email); if (user) { throw new HttpException( - "User already exist in the system", + i18n.t("addUser.createExistingUser"), HttpStatus.BAD_REQUEST ); } let { company, ...userFields } = userDto; if (company) { - if (userFields.role && ![Role.Admin, Role.Root].includes(userFields.role)) { + if ( + userFields.role && + ![Role.Admin, Role.Root].includes(userFields.role) + ) { throw new HttpException( - "Company create user should be an Admin user", + i18n.t("addUser.companyCreateUserShouldbeAdmin"), HttpStatus.BAD_REQUEST ); } else if (!userFields.role) { @@ -313,7 +323,7 @@ export class UserService { if (companyRole != CompanyRole.GOVERNMENT) { throw new HttpException( - "Company create does not permitted for your company role", + i18n.t("addUser.companyCreateNotPermittedForTheCompanyRole"), HttpStatus.FORBIDDEN ); } @@ -327,7 +337,7 @@ export class UserService { userDto.companyId != companyId ) { throw new HttpException( - "Not authorized to add users to other companies", + i18n.t("addUser.createUserToOtherCompaniesUnAuth"), HttpStatus.FORBIDDEN ); } @@ -338,7 +348,10 @@ export class UserService { } else if (u.companyId) { const company = await this.companyService.findByCompanyId(u.companyId); if (!company) { - throw new HttpException("Invalid programme id", HttpStatus.BAD_REQUEST); + throw new HttpException( + i18n.t("addUser.addUserToUnRegisteredCompany"), + HttpStatus.BAD_REQUEST + ); } u.companyRole = company.companyRole; } else { @@ -369,18 +382,22 @@ export class UserService { company.logo = response.Location; } else { throw new HttpException( - "Company update failed. Please try again", + i18n.t("addUser.companyUpdateFailed"), HttpStatus.INTERNAL_SERVER_ERROR ); } - if(company.email){ - await this.emailService.sendEmail(company.email, EmailTemplates.ORGANISATION_CREATE, { - organisationName: company.name, - countryName: this.configService.get("systemCountryName"), - organisationRole: company.companyRole, - home: hostAddress, - }); + if (company.email) { + await this.emailService.sendEmail( + company.email, + EmailTemplates.ORGANISATION_CREATE, + { + organisationName: company.name, + countryName: this.configService.get("systemCountryName"), + organisationRole: company.companyRole, + home: hostAddress, + } + ); } } @@ -462,7 +479,7 @@ export class UserService { "company.companyId = user.companyId" ) .orderBy( - query?.sort?.key ? `"user"."${query?.sort?.key}"` : `"user"."id"` , + query?.sort?.key ? `"user"."${query?.sort?.key}"` : `"user"."id"`, query?.sort?.order ? query?.sort?.order : "DESC" ) .offset(query.size * query.page - query.size) @@ -525,9 +542,14 @@ export class UserService { async getGovAdminAndManagerUsers() { const result = await this.userRepo .createQueryBuilder("user") - .where("user.role in (:admin, :manager)",{admin:Role.Admin, manager:Role.Manager}) - .andWhere("user.companyRole= :companyRole",{companyRole:CompanyRole.GOVERNMENT}) - .select(['user.name','user.email']) + .where("user.role in (:admin, :manager)", { + admin: Role.Admin, + manager: Role.Manager, + }) + .andWhere("user.companyRole= :companyRole", { + companyRole: CompanyRole.GOVERNMENT, + }) + .select(["user.name", "user.email"]) .getRawMany(); return result; @@ -538,9 +560,12 @@ export class UserService { async getOrganisationAdminAndManagerUsers(organisationId) { const result = await this.userRepo .createQueryBuilder("user") - .where("user.role in (:admin,:manager)",{admin:Role.Admin, manager:Role.Manager}) - .andWhere("user.companyId= :companyId",{companyId:organisationId}) - .select(['user.name','user.email']) + .where("user.role in (:admin,:manager)", { + admin: Role.Admin, + manager: Role.Manager, + }) + .andWhere("user.companyId= :companyId", { companyId: organisationId }) + .select(["user.name", "user.email"]) .getRawMany(); return result; diff --git a/lambda/services/src/shared/util/helpers.service.ts b/lambda/services/src/shared/util/helpers.service.ts index 550564c07..17c9ccc59 100644 --- a/lambda/services/src/shared/util/helpers.service.ts +++ b/lambda/services/src/shared/util/helpers.service.ts @@ -7,6 +7,7 @@ import { Stat } from "../dto/stat.dto"; import { programmeStatusRequestDto } from "../dto/programmeStatus.request.dto"; import { chartStatsRequestDto } from "../dto/chartStats.request.dto"; import { ConfigService } from "@nestjs/config"; +import { I18n, I18nContext } from "nestjs-i18n"; @Injectable() export class HelperService { @@ -29,29 +30,56 @@ export class HelperService { private prepareKey(col: string, table?: string) { let key; - if (col.includes('->>')) { - const parts = col.split('->>'); - key = `"${parts[0]}"->>'${parts[1]}'` + if (col.includes("->>")) { + const parts = col.split("->>"); + key = `"${parts[0]}"->>'${parts[1]}'`; } else { - key = `"${col}"` + key = `"${col}"`; } - return `${table ? table + "." : ""}${key}` + return `${table ? table + "." : ""}${key}`; } private isLower(key: string) { - if (["email", "name", "companyName", "taxId", "country", "title", "externalId", "serialNo", "programmeTitle"].includes(key)) + if ( + [ + "email", + "name", + "companyName", + "taxId", + "country", + "title", + "externalId", + "serialNo", + "programmeTitle", + ].includes(key) + ) return true; } public generateSortCol(col: string) { - if (col.includes('->>')) { - const parts = col.split('->>'); - return `"${parts[0]}"->>'${parts[1]}'` + if (col.includes("->>")) { + const parts = col.split("->>"); + return `"${parts[0]}"->>'${parts[1]}'`; } else { - return `"${col}"` + return `"${col}"`; } } + public formatReqMessagesString( + i18n: I18nContext, + langTag: string, + vargs: any[] + ) { + const str: any = i18n.t(langTag); + const parts: any = str.split("{}"); + let insertAt = 1; + for (const arg of vargs) { + parts.splice(insertAt, 0, arg); + insertAt += 2; + } + return parts.join(""); + } + public generateWhereSQLChartStastics( data: chartStatsRequestDto, extraSQL: string, @@ -104,7 +132,11 @@ export class HelperService { } private isQueryDto(obj) { - if (obj && typeof obj === "object" && (obj["filterAnd"] || obj["filterOr"])) { + if ( + obj && + typeof obj === "object" && + (obj["filterAnd"] || obj["filterOr"]) + ) { return true; } return false; @@ -231,8 +263,11 @@ export class HelperService { .map((e) => { if (this.isQueryDto(e.value)) { return `(${this.prepareValue(e.value, table)})`; - } else if (e.operation === 'ANY') { - return `${this.prepareValue(e.value, table)} = ANY(${this.prepareKey(e.key, table)})`; + } else if (e.operation === "ANY") { + return `${this.prepareValue( + e.value, + table + )} = ANY(${this.prepareKey(e.key, table)})`; } else if (e.keyOperation) { return `${e.keyOperation}(${this.prepareKey(e.key, table)}) ${ e.operation @@ -254,16 +289,23 @@ export class HelperService { .map((e) => { if (this.isQueryDto(e.value)) { return `(${this.prepareValue(e.value, table)})`; - } else if (e.operation === 'ANY') { - return `${this.prepareValue(e.value, table)} = ANY(${this.prepareKey(e.key, table)})`; + } else if (e.operation === "ANY") { + return `${this.prepareValue( + e.value, + table + )} = ANY(${this.prepareKey(e.key, table)})`; } else if (e.keyOperation) { return `${e.keyOperation}(${this.prepareKey(e.key, table)}) ${ e.operation } ${this.prepareValue(e.value, table, true)}`; } else if (this.isLower(e.key) && typeof e.value === "string") { - return `LOWER(${this.prepareKey(e.key, table)}) ${e.operation} ${this.prepareValue(e.value, table, true)}`; + return `LOWER(${this.prepareKey(e.key, table)}) ${ + e.operation + } ${this.prepareValue(e.value, table, true)}`; } else { - return `${this.prepareKey(e.key, table)} ${e.operation} ${this.prepareValue(e.value, table)}`; + return `${this.prepareKey(e.key, table)} ${ + e.operation + } ${this.prepareValue(e.value, table)}`; } }) .join(" or "); @@ -364,7 +406,7 @@ export class HelperService { ContentEncoding: "base64", ContentType: "image/png", }; - + uploadParams.Key = `profile_images/${companyId}_${new Date().getTime()}.png`; return await s3 diff --git a/web/src/App.tsx b/web/src/App.tsx index 43330b81e..60bda89a4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -58,7 +58,7 @@ const App = () => { serverURL={ process.env.REACT_APP_BACKEND ? process.env.REACT_APP_BACKEND - : 'https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev' + : 'http://localhost:3000/local' } > From b2689dddfad1bfbc228488e3108a96db4bbcfba8 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 16:10:29 +0530 Subject: [PATCH 15/68] changes in error messages in reset password --- lambda/services/src/i18n/en/addUser.json | 3 ++- lambda/services/src/national-api/user.controller.ts | 9 +++++++-- lambda/services/src/shared/user/user.service.ts | 7 ++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lambda/services/src/i18n/en/addUser.json b/lambda/services/src/i18n/en/addUser.json index d91909566..e9deb2424 100644 --- a/lambda/services/src/i18n/en/addUser.json +++ b/lambda/services/src/i18n/en/addUser.json @@ -11,5 +11,6 @@ "governmentUserAlreadyExist": "Government already exist for the country code {}", "companyCreateNotPermittedForTheCompanyRole": "Company create does not permitted for your company role", "companyUpdateFailed": "Company update failed. Please try again", - "propValueAlreadyExist": "{} already exist" + "propValueAlreadyExist": "{} already exist", + "taxIdExistAlready": "Company tax id already exist" } \ No newline at end of file diff --git a/lambda/services/src/national-api/user.controller.ts b/lambda/services/src/national-api/user.controller.ts index 8d158f533..b84f9aec0 100644 --- a/lambda/services/src/national-api/user.controller.ts +++ b/lambda/services/src/national-api/user.controller.ts @@ -79,11 +79,16 @@ export class UserController { @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) @Put("resetPassword") - resetPassword(@Body() reset: PasswordUpdateDto, @Request() req) { + resetPassword( + @Body() reset: PasswordUpdateDto, + @Request() req, + @I18n() i18n: I18nContext + ) { return this.userService.resetPassword( req.user.id, reset, - req.abilityCondition + req.abilityCondition, + i18n ); } diff --git a/lambda/services/src/shared/user/user.service.ts b/lambda/services/src/shared/user/user.service.ts index cc9bdff96..77998eeab 100644 --- a/lambda/services/src/shared/user/user.service.ts +++ b/lambda/services/src/shared/user/user.service.ts @@ -174,7 +174,8 @@ export class UserService { async resetPassword( id: number, passwordResetDto: PasswordUpdateDto, - abilityCondition: string + abilityCondition: string, + i18n?: I18nContext ) { this.logger.verbose("User password reset received", id); @@ -192,7 +193,7 @@ export class UserService { .getOne(); if (!user || user.password != passwordResetDto.oldPassword) { throw new HttpException( - "Old Password is incorrect", + i18n.t("resetPassword.incorrectCurrentPassword"), HttpStatus.UNAUTHORIZED ); } @@ -442,7 +443,7 @@ export class UserService { ); } else if (err.driverError.detail.includes("taxId")) { throw new HttpException( - "Company tax id already exist", + i18n.t("addUser.taxIdExistAlready"), HttpStatus.BAD_REQUEST ); } From 735a722b1ea92f38c4c1af860e9095d51aca6773 Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 16:15:16 +0530 Subject: [PATCH 16/68] spell changes in certifier view stat card --- web/public/Assets/i18n/dashboard/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/Assets/i18n/dashboard/en.json b/web/public/Assets/i18n/dashboard/en.json index 18de9ae71..bedf5dbc2 100644 --- a/web/public/Assets/i18n/dashboard/en.json +++ b/web/public/Assets/i18n/dashboard/en.json @@ -13,7 +13,7 @@ "programmesUnCertified": "Programmes Certifiable", "programmesCertified": "Programmes Certified", "creditBal": "Credit Balance", - "creditCertified": "Credit Certified", + "creditCertified": "Credits Certified", "programmes": "Programmes", "credits": "Credits", "certifiedCredits": "Certified Credits", From 03bbc16aa4c64d7d2b80b7d8214e9c59b2b7badc Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Mon, 27 Feb 2023 16:56:17 +0530 Subject: [PATCH 17/68] Deleted unwanted config file in dashboard section --- web/src/App.tsx | 2 +- web/src/Pages/Dashboard/config.json | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 web/src/Pages/Dashboard/config.json diff --git a/web/src/App.tsx b/web/src/App.tsx index 60bda89a4..43330b81e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -58,7 +58,7 @@ const App = () => { serverURL={ process.env.REACT_APP_BACKEND ? process.env.REACT_APP_BACKEND - : 'http://localhost:3000/local' + : 'https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev' } > diff --git a/web/src/Pages/Dashboard/config.json b/web/src/Pages/Dashboard/config.json deleted file mode 100644 index 5399d63f5..000000000 --- a/web/src/Pages/Dashboard/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "token": "pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4M29iazA2Z2gycXA4N2pmbDZmangifQ.-g_vE53SD2WrJ6tFX7QHmA", - "londonCycle": "mapbox://styles/mapbox/light-v9", - "light": "mapbox://styles/mapbox/light-v9", - "dark": "mapbox://styles/mapbox/dark-v9", - "basic": "mapbox://styles/mapbox/basic-v9", - "outdoor": "mapbox://styles/mapbox/outdoors-v10" -} From 05e3fe9183f17bbcc79c9fdaa08e3feb8712678c Mon Sep 17 00:00:00 2001 From: dhanushkaxep Date: Tue, 28 Feb 2023 10:46:14 +0530 Subject: [PATCH 18/68] tempaory disabed email sending --- .../src/shared/email/email.service.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lambda/services/src/shared/email/email.service.ts b/lambda/services/src/shared/email/email.service.ts index e19daf4c8..0644d09c8 100644 --- a/lambda/services/src/shared/email/email.service.ts +++ b/lambda/services/src/shared/email/email.service.ts @@ -56,26 +56,26 @@ export class EmailService { public async sendEmail(sendToEmail: string, template, templateData: any): Promise { this.logger.log('Sending email', JSON.stringify(sendToEmail)) // this.configService.get('stage') != 'local' && !sendToEmail.endsWith(this.configService.get('email.skipSuffix')) - if (sendToEmail && !sendToEmail.endsWith('@xeptagon.com')) { - return new Promise((resolve, reject) => { - this.transporter.sendMail({ - from: this.sourceEmail, - to: sendToEmail, - subject: this.getSubjectMessage(template["subject"], templateData), - text: this.getTemplateMessage(template["text"], templateData), // plain text body - html: this.getTemplateMessage(template["html"], templateData), // html body - }, function(error, info) { - if (error) { - console.log(error); - reject(error) - } else { - console.log('Email sent: ' + info); - resolve(info) - } - }); - }) - } else { - this.logger.log('Skipped email due to local email', sendToEmail); - } + // if (sendToEmail && !sendToEmail.endsWith('@xeptagon.com')) { + // return new Promise((resolve, reject) => { + // this.transporter.sendMail({ + // from: this.sourceEmail, + // to: sendToEmail, + // subject: this.getSubjectMessage(template["subject"], templateData), + // text: this.getTemplateMessage(template["text"], templateData), // plain text body + // html: this.getTemplateMessage(template["html"], templateData), // html body + // }, function(error, info) { + // if (error) { + // console.log(error); + // reject(error) + // } else { + // console.log('Email sent: ' + info); + // resolve(info) + // } + // }); + // }) + // } else { + // this.logger.log('Skipped email due to local email', sendToEmail); + // } } } From 0e5ba39f3c070a003b42880d25a24aa7a9e7deec Mon Sep 17 00:00:00 2001 From: dhanushkaxep Date: Tue, 28 Feb 2023 13:44:34 +0530 Subject: [PATCH 19/68] email changes --- lambda/services/package.json | 1 + lambda/services/src/shared/configuration.ts | 3 +- .../email-helper/email-helper.service.ts | 15 ++- .../src/shared/email/email.service.ts | 3 +- .../src/shared/email/email.template.ts | 92 +++++++++---------- .../src/shared/programme/programme.service.ts | 5 +- lambda/services/yarn.lock | 5 + 7 files changed, 73 insertions(+), 51 deletions(-) diff --git a/lambda/services/package.json b/lambda/services/package.json index be9c89752..71f623290 100644 --- a/lambda/services/package.json +++ b/lambda/services/package.json @@ -47,6 +47,7 @@ "fs": "^0.0.1-security", "ion-js": "^4.3.0", "jsbi": "^4.3.0", + "moment": "^2.29.4", "nanoid": "3.3.4", "nest-winston": "^1.8.0", "nodemailer": "^6.8.0", diff --git a/lambda/services/src/shared/configuration.ts b/lambda/services/src/shared/configuration.ts index 1ecd084e1..a2dde2364 100644 --- a/lambda/services/src/shared/configuration.ts +++ b/lambda/services/src/shared/configuration.ts @@ -31,7 +31,8 @@ export default () => ({ endpoint: process.env.EMAIL_ENDPOINT || 'vpce-02cef9e74f152b675-b00ybiai.email-smtp.us-east-1.vpce.amazonaws.com', username: process.env.EMAIL_USERNAME || 'AKIAUMXKTXDJIOFY2QXL', password: process.env.SES_PASSWORD, - skipSuffix: '@xeptagon.com' + skipSuffix: '@xeptagon.com', + Disabled: process.env.IS_EMAIL_DISABLED === 'true' ? true : false }, s3CommonBucket: { name: 'carbon-common-'+ (process.env.NODE_ENV || 'dev'), diff --git a/lambda/services/src/shared/email-helper/email-helper.service.ts b/lambda/services/src/shared/email-helper/email-helper.service.ts index 5dc434f67..5da40097f 100644 --- a/lambda/services/src/shared/email-helper/email-helper.service.ts +++ b/lambda/services/src/shared/email-helper/email-helper.service.ts @@ -9,6 +9,8 @@ import { UserService } from "../user/user.service"; @Injectable() export class EmailHelperService { + isEmailDisabled: boolean; + constructor( @Inject(forwardRef(() => UserService)) private userService: UserService, @@ -17,7 +19,9 @@ export class EmailHelperService { @Inject(forwardRef(() => CompanyService)) private companyService: CompanyService, private programmeLedger: ProgrammeLedgerService - ) {} + ) { + this.isEmailDisabled = this.configService.get("email.Disabled"); + } public async sendEmailToProgrammeOwnerAdmins( programmeId: string, @@ -26,6 +30,9 @@ export class EmailHelperService { companyId?: number, governmentId?: number ) { + + if(this.isEmailDisabled) + return; const programme = await this.programmeLedger.getProgrammeById(programmeId); const hostAddress = this.configService.get("host"); let companyDetails: Company; @@ -96,6 +103,8 @@ export class EmailHelperService { receiverCompanyId?: number, programmeId?: string ) { + if(this.isEmailDisabled) + return; const systemCountryName = this.configService.get("systemCountryName"); const users = await this.userService.getOrganisationAdminAndManagerUsers( companyId @@ -244,6 +253,8 @@ export class EmailHelperService { programmeId?: string, companyId?: number ) { + if(this.isEmailDisabled) + return; const systemCountryName = this.configService.get("systemCountryName"); const hostAddress = this.configService.get("host"); const users = await this.userService.getGovAdminAndManagerUsers(); @@ -309,6 +320,8 @@ export class EmailHelperService { templateData: any, companyId: number ) { + if(this.isEmailDisabled) + return; const companyDetails = await this.companyService.findByCompanyId(companyId); const systemCountryName = this.configService.get("systemCountryName"); templateData = { diff --git a/lambda/services/src/shared/email/email.service.ts b/lambda/services/src/shared/email/email.service.ts index 0644d09c8..a9f055687 100644 --- a/lambda/services/src/shared/email/email.service.ts +++ b/lambda/services/src/shared/email/email.service.ts @@ -56,7 +56,8 @@ export class EmailService { public async sendEmail(sendToEmail: string, template, templateData: any): Promise { this.logger.log('Sending email', JSON.stringify(sendToEmail)) // this.configService.get('stage') != 'local' && !sendToEmail.endsWith(this.configService.get('email.skipSuffix')) - // if (sendToEmail && !sendToEmail.endsWith('@xeptagon.com')) { + // const isEmailDisabled = this.configService.get("email.Disabled"); + // if (!isEmailDisabled && sendToEmail && !sendToEmail.endsWith('@xeptagon.com')) { // return new Promise((resolve, reject) => { // this.transporter.sendMail({ // from: this.sourceEmail, diff --git a/lambda/services/src/shared/email/email.template.ts b/lambda/services/src/shared/email/email.template.ts index e6d256739..faca5182f 100644 --- a/lambda/services/src/shared/email/email.template.ts +++ b/lambda/services/src/shared/email/email.template.ts @@ -85,12 +85,12 @@ export const EmailTemplates = { }, CHANGE_PASSOWRD: { id: 'CHANGE_PASSOWRD', - subject: 'Your password was changed', + subject: 'Your Password was Changed', html: ` Hi {{name}},

The password of your Carbon Registry account was changed successfully.

If you do not use {{countryName}} Carbon Credit Registry or did not request a password reset, please ignore this email or - contact support + contact support if you have questions.

@@ -116,14 +116,14 @@ export const EmailTemplates = { }, PROGRAMME_AUTHORISATION: { id: 'PROGRAMME_AUTHORISATION', - subject: 'Programme authorised', + subject: 'Programme Authorised', html:` Hi {{name}},

- {{programmeName}} of your organisation has been authorised on {{authorisedDate}} with the serial number {{serialNumber}}. + {{programmeName}} of your Organisation has been authorised on {{authorisedDate}} with the serial number {{serialNumber}}.

- Click here for more details of the programme. + Click here for more details of the programme.

Sincerely,
@@ -139,7 +139,7 @@ export const EmailTemplates = { {{programmeName}} of your Organisation has been rejected on {{date}} due to the following reason/s:
{{reason}}

- Click here {{pageLink}} for more details of the programme.

+ Click here for more details of the programme.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -153,7 +153,7 @@ export const EmailTemplates = { {{programmeName}} of your Organisation with the serial number {{serialNumber}} has been issued with {{credits}} credits.

- Click here {{pageLink}} for more details of the programme.

+ Click here for more details of the programme.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -166,7 +166,7 @@ export const EmailTemplates = { Hi {{name}},

{{organisationName}} has requested to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}}.

- Click here {{pageLink}} for more details of the transfer request.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -179,7 +179,7 @@ export const EmailTemplates = { Hi {{name}},

Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by {{organisationName}} has been cancelled.

- Click here {{pageLink}} for more details of the transfer request.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -191,8 +191,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been accepted by {{organisationName}}.

- Click here {{pageLink}} for more details of the transfer request.

+ Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your Organisation has been accepted by {{organisationName}}.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -205,8 +205,8 @@ export const EmailTemplates = { Hi {{name}},

Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} - made by your organisation has been rejected by {{organisationName}}.

- Click here {{pageLink}} for more details of the transfer request.

+ made by your Organisation has been rejected by {{organisationName}}.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team` @@ -217,10 +217,10 @@ export const EmailTemplates = { html: ` Hi {{name}},

- {{government}} has requested your organisation to transfer {{credits}} credits with the serial number {{serialNumber}} + {{government}} has requested your Organisation to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{organisationName}}.

- Click here {{pageLink}} for more details of the transfer request.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -235,7 +235,7 @@ export const EmailTemplates = { Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{organisationName}} made by {{government}} has been cancelled.

- Click here {{pageLink}} for more details of the transfer request.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -247,8 +247,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been accepted by {{organisationName}}.

- Click here {{pageLink}} for more details of the transfer request.

+ Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your Organisation has been accepted by {{organisationName}}.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -260,8 +260,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your organisation by accepting the request made by the {{government}}.

- Click here {{pageLink}} for more details of the transfer request

+ {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your Organisation by accepting the request made by the {{government}}.

+ Click here for more details of the transfer request

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -273,8 +273,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been rejected by {{organisationName}}.

- Click here {{pageLink}} for more details of the transfer request

+ Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your Organisation has been rejected by {{organisationName}}.

+ Click here for more details of the transfer request

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -286,9 +286,9 @@ export const EmailTemplates = { html: ` Hi {{name}},

- {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your organisation.

+ {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your Organisation.

- Click here {{pageLink}} for more details of the transfer request.

+ Click here for more details of the transfer request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -301,7 +301,7 @@ export const EmailTemplates = { Hi {{name}},

The {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} of your Organisation has been certified by {{organisationName}}.

- Click here {{pageLink}} for more details of the certification.

+ Click here for more details of the certification.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -314,7 +314,7 @@ export const EmailTemplates = { Hi {{name}},

The certification of the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by {{organisationName}}.

- Click here {{pageLink}} for more details of the certification.

+ Click here for more details of the certification.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -327,7 +327,7 @@ export const EmailTemplates = { Hi {{name}},

The certification given by {{organisationName}} for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the {{government}}.

- Click here {{pageLink}} for more details of the certification.

+ Click here for more details of the certification.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -339,8 +339,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- The certification given by your organisation for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the {{government}}.

- Click here {{pageLink}} for more details of the certification.

+ The certification given by your Organisation for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the {{government}}.

+ Click here for more details of the certification.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -353,7 +353,7 @@ export const EmailTemplates = { Hi {{name}},

The certification given by {{organisationName}} for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the system as {{organisationName}} was deactivated.

- Click here {{pageLink}} for more details of the certification.

+ Click here for more details of the certification.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -365,11 +365,11 @@ export const EmailTemplates = { html: ` Hi,

- Your organisation has been deactivated by the {{government}}. Your organisation will still be visible but not other will be able to take place. Following are the effects of deactivation:

- · All the users of the organisation were deactivated.
- · All the credits owned by your organisation were frozen.
- · All credit transfer requests sent and received by your organisation were cancelled.
- · All the international transfer retire requests sent by your organisation were cancelled.

+ Your Organisation has been deactivated by the {{government}}. Your Organisation will still be visible but not other will be able to take place. Following were the effects of deactivation:

+ · All the users of the Organisation were deactivated.
+ · All the credits owned by your Organisation were frozen.
+ · All credit transfer requests sent and received by your Organisation were cancelled.
+ · All the international transfer retire requests sent by your Organisation were cancelled.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -381,9 +381,9 @@ export const EmailTemplates = { html: ` Hi,

- Your organisation has been deactivated by the {{government}}. Your organisation will still be visible but not other will be able to take place. Following are the effects of deactivation:

- · All the users of the organisation were deactivated.
- · All the certificates given by your organisation were revoked.

+ Your Organisation has been deactivated by the {{government}}. Your Organisation will still be visible but no further action will be able to take place. Following are the effects of deactivation:

+ · All the users of the Organisation were deactivated.
+ · All the certificates given by your Organisation were revoked.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -396,7 +396,7 @@ export const EmailTemplates = { Hi {{name}},

{{credits}} credits of the programme {{programmeName}} with the serial number {{serialNumber}} has been retired by the {{government}} as {{reason}}.

- Click here {{pageLink}} for more details of the retirement.

+ Click here for more details of the retirement.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -409,7 +409,7 @@ export const EmailTemplates = { Hi {{name}},

{{organisationName}} has requested an international transfer retirement of {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}}.

- Click here {{pageLink}} for more details of the international transfer retire request.

+ Click here for more details of the international transfer retire request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -422,7 +422,7 @@ export const EmailTemplates = { Hi {{name}},

Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by {{organisationName}} has been cancelled.

- Click here {{pageLink}} for more details of the international transfer retire request.

+ Click here for more details of the international transfer retire request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -434,8 +434,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your organisation has been recognised.

- Click here {{pageLink}} for more details of the international transfer retire request.

+ Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your Organisation has been recognised.

+ Click here for more details of the international transfer retire request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -447,8 +447,8 @@ export const EmailTemplates = { html: ` Hi {{name}},

- Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your organisation has not been recognised.

- Click here {{pageLink}} for more details of the international transfer retire request.

+ Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your Organisation has not been recognised.

+ Click here for more details of the international transfer retire request.

Sincerely,
The {{countryName}} Carbon Credit Registry Team @@ -460,7 +460,7 @@ export const EmailTemplates = { html: ` Hi

- Your organisation has been reactivated by the {{government}}. Your organisation will be able to perform actions as before and all the users of the organisation will be reactivated.

+ Your Organisation has been reactivated by the {{government}}. Your Organisation will be able to perform actions as before and all the users of the Organisation will be reactivated.

Sincerely,
The {{countryName}} Carbon Credit Registry Team diff --git a/lambda/services/src/shared/programme/programme.service.ts b/lambda/services/src/shared/programme/programme.service.ts index 97082945a..f0f5e2b72 100644 --- a/lambda/services/src/shared/programme/programme.service.ts +++ b/lambda/services/src/shared/programme/programme.service.ts @@ -51,6 +51,7 @@ import { UserService } from "../user/user.service"; import { use } from "passport"; import { SystemActionType } from "../enum/system.action.type"; import { CountryService } from "../util/country.service"; +import moment from 'moment'; export declare function PrimaryGeneratedColumn( options: PrimaryGeneratedColumnType @@ -1531,7 +1532,7 @@ export class ProgrammeService { EmailTemplates.CREDIT_ISSUANCE, { programmeName: updated.title, - credits: updated.creditIssued, + credits: req.issueAmount, serialNumber: updated.serialNo, pageLink: hostAddress + `/programmeManagement/view?id=${updated.programmeId}`, @@ -1598,7 +1599,7 @@ export class ProgrammeService { EmailTemplates.PROGRAMME_AUTHORISATION, { programmeName: updated.title, - authorisedDate: new Date(updated.txTime), + authorisedDate: moment(updated.txTime).format("DD MMMM YYYY"), serialNumber: updated.serialNo, programmePageLink: hostAddress + `/programmeManagement/view?id=${updated.programmeId}`, diff --git a/lambda/services/yarn.lock b/lambda/services/yarn.lock index eced90049..4bb15c662 100644 --- a/lambda/services/yarn.lock +++ b/lambda/services/yarn.lock @@ -8167,6 +8167,11 @@ module-deps@^6.2.3: through2 "^2.0.0" xtend "^4.0.0" +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" From efef8db9776cffe85ce15afcc140177aa37dfb98 Mon Sep 17 00:00:00 2001 From: palindaa <99331873+palindaa@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:27:14 +0530 Subject: [PATCH 20/68] Update programmeView.tsx --- web/src/Pages/ProgrammeView/programmeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/Pages/ProgrammeView/programmeView.tsx b/web/src/Pages/ProgrammeView/programmeView.tsx index c5d99b7ce..d8efe3985 100644 --- a/web/src/Pages/ProgrammeView/programmeView.tsx +++ b/web/src/Pages/ProgrammeView/programmeView.tsx @@ -409,7 +409,7 @@ const ProgrammeView = () => { transfer.isRetirement && transfer.toCompanyMeta?.countryName ? transfer.toCompanyMeta?.countryName : transfer.receiver[0]?.name, - transfer.requester[0]?.name, + transfer.isRetirement ? transfer.receiver[0]?.name : transfer.sender[0]?.name,, ] )} remark={transfer.txRef?.split('#')[0]} From cc6e8791226583a41aae2e6cbf33e25ca7392f51 Mon Sep 17 00:00:00 2001 From: palindaa <99331873+palindaa@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:34:41 +0530 Subject: [PATCH 21/68] Update programmeView.tsx --- web/src/Pages/ProgrammeView/programmeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/Pages/ProgrammeView/programmeView.tsx b/web/src/Pages/ProgrammeView/programmeView.tsx index d8efe3985..4e2f6be3a 100644 --- a/web/src/Pages/ProgrammeView/programmeView.tsx +++ b/web/src/Pages/ProgrammeView/programmeView.tsx @@ -409,7 +409,7 @@ const ProgrammeView = () => { transfer.isRetirement && transfer.toCompanyMeta?.countryName ? transfer.toCompanyMeta?.countryName : transfer.receiver[0]?.name, - transfer.isRetirement ? transfer.receiver[0]?.name : transfer.sender[0]?.name,, + transfer.isRetirement ? transfer.receiver[0]?.name : transfer.sender[0]?.name, ] )} remark={transfer.txRef?.split('#')[0]} From 566c1e839bc4a3c63dcb6fe059a3423014eb2dbb Mon Sep 17 00:00:00 2001 From: Yathurshan Thurairajah Date: Wed, 1 Mar 2023 10:38:25 +0530 Subject: [PATCH 22/68] CHanged the way of localization implementaion by nest-i18n - using one common function defined in helper service file --- .../src/national-api/user.controller.ts | 23 +++++---- .../services/src/shared/user/user.service.ts | 47 ++++++++++++++----- .../src/shared/util/helpers.service.ts | 15 +++--- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/lambda/services/src/national-api/user.controller.ts b/lambda/services/src/national-api/user.controller.ts index b84f9aec0..63414715d 100644 --- a/lambda/services/src/national-api/user.controller.ts +++ b/lambda/services/src/national-api/user.controller.ts @@ -29,6 +29,7 @@ import { UserUpdateDto } from "../shared/dto/user.update.dto"; import { PasswordUpdateDto } from "../shared/dto/password.update.dto"; import { Role } from "../shared/casl/role.enum"; import { JwtAuthGuard } from "../shared/auth/guards/jwt-auth.guard"; +import { HelperService } from "../shared/util/helpers.service"; @ApiTags("User") @ApiBearerAuth() @@ -36,7 +37,8 @@ import { JwtAuthGuard } from "../shared/auth/guards/jwt-auth.guard"; export class UserController { constructor( private readonly userService: UserService, - private caslAbilityFactory: CaslAbilityFactory + private caslAbilityFactory: CaslAbilityFactory, + private helperService: HelperService ) {} @ApiBearerAuth() @@ -52,18 +54,20 @@ export class UserController { ability.can(Action.Create, Object.assign(new User(), body)) ) @Post("add") - addUser(@Body() user: UserDto, @Request() req, @I18n() i18n: I18nContext) { + addUser(@Body() user: UserDto, @Request() req) { if (user.role == Role.Root) { throw new HttpException( - i18n.t("addUser.rootCreatesRoot"), + this.helperService.formatReqMessagesString( + "addUser.rootCreatesRoot", + [] + ), HttpStatus.FORBIDDEN ); } return this.userService.create( user, req.user.companyId, - req.user.companyRole, - i18n + req.user.companyRole ); } @@ -79,16 +83,11 @@ export class UserController { @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, User, true)) // @CheckPolicies((ability, body) => ability.can(Action.Update, Object.assign(new User(), body))) @Put("resetPassword") - resetPassword( - @Body() reset: PasswordUpdateDto, - @Request() req, - @I18n() i18n: I18nContext - ) { + resetPassword(@Body() reset: PasswordUpdateDto, @Request() req) { return this.userService.resetPassword( req.user.id, reset, - req.abilityCondition, - i18n + req.abilityCondition ); } diff --git a/lambda/services/src/shared/user/user.service.ts b/lambda/services/src/shared/user/user.service.ts index 77998eeab..c8596c93e 100644 --- a/lambda/services/src/shared/user/user.service.ts +++ b/lambda/services/src/shared/user/user.service.ts @@ -12,7 +12,6 @@ import { InjectEntityManager, InjectRepository, } from "@nestjs/typeorm"; -import { I18n, I18nContext } from "nestjs-i18n"; import { UserDto } from "../../shared/dto/user.dto"; import { Connection, @@ -174,8 +173,7 @@ export class UserService { async resetPassword( id: number, passwordResetDto: PasswordUpdateDto, - abilityCondition: string, - i18n?: I18nContext + abilityCondition: string ) { this.logger.verbose("User password reset received", id); @@ -193,7 +191,10 @@ export class UserService { .getOne(); if (!user || user.password != passwordResetDto.oldPassword) { throw new HttpException( - i18n.t("resetPassword.incorrectCurrentPassword"), + this.helperService.formatReqMessagesString( + "resetPassword.incorrectCurrentPassword", + [] + ), HttpStatus.UNAUTHORIZED ); } @@ -279,15 +280,17 @@ export class UserService { async create( userDto: UserDto, companyId: number, - companyRole: CompanyRole, - i18n?: I18nContext + companyRole: CompanyRole ): Promise { this.logger.verbose(`User create received ${userDto.email} ${companyId}`); const user = await this.findOne(userDto.email); if (user) { throw new HttpException( - i18n.t("addUser.createExistingUser"), + this.helperService.formatReqMessagesString( + "addUser.createExistingUser", + [] + ), HttpStatus.BAD_REQUEST ); } @@ -299,7 +302,10 @@ export class UserService { ![Role.Admin, Role.Root].includes(userFields.role) ) { throw new HttpException( - i18n.t("addUser.companyCreateUserShouldbeAdmin"), + this.helperService.formatReqMessagesString( + "addUser.companyCreateUserShouldbeAdmin", + [] + ), HttpStatus.BAD_REQUEST ); } else if (!userFields.role) { @@ -324,7 +330,10 @@ export class UserService { if (companyRole != CompanyRole.GOVERNMENT) { throw new HttpException( - i18n.t("addUser.companyCreateNotPermittedForTheCompanyRole"), + this.helperService.formatReqMessagesString( + "addUser.companyCreateNotPermittedForTheCompanyRole", + [] + ), HttpStatus.FORBIDDEN ); } @@ -338,7 +347,10 @@ export class UserService { userDto.companyId != companyId ) { throw new HttpException( - i18n.t("addUser.createUserToOtherCompaniesUnAuth"), + this.helperService.formatReqMessagesString( + "createUserToOtherCompaniesUnAuth", + [] + ), HttpStatus.FORBIDDEN ); } @@ -350,7 +362,10 @@ export class UserService { const company = await this.companyService.findByCompanyId(u.companyId); if (!company) { throw new HttpException( - i18n.t("addUser.addUserToUnRegisteredCompany"), + this.helperService.formatReqMessagesString( + "addUser.addUserToUnRegisteredCompany", + [] + ), HttpStatus.BAD_REQUEST ); } @@ -383,7 +398,10 @@ export class UserService { company.logo = response.Location; } else { throw new HttpException( - i18n.t("addUser.companyUpdateFailed"), + this.helperService.formatReqMessagesString( + "addUser.companyUpdateFailed", + [] + ), HttpStatus.INTERNAL_SERVER_ERROR ); } @@ -443,7 +461,10 @@ export class UserService { ); } else if (err.driverError.detail.includes("taxId")) { throw new HttpException( - i18n.t("addUser.taxIdExistAlready"), + this.helperService.formatReqMessagesString( + "addUser.taxIdExistAlready", + [] + ), HttpStatus.BAD_REQUEST ); } diff --git a/lambda/services/src/shared/util/helpers.service.ts b/lambda/services/src/shared/util/helpers.service.ts index 17c9ccc59..6ca337108 100644 --- a/lambda/services/src/shared/util/helpers.service.ts +++ b/lambda/services/src/shared/util/helpers.service.ts @@ -7,11 +7,14 @@ import { Stat } from "../dto/stat.dto"; import { programmeStatusRequestDto } from "../dto/programmeStatus.request.dto"; import { chartStatsRequestDto } from "../dto/chartStats.request.dto"; import { ConfigService } from "@nestjs/config"; -import { I18n, I18nContext } from "nestjs-i18n"; +import { I18n, I18nContext, I18nService } from "nestjs-i18n"; @Injectable() export class HelperService { - constructor(private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private i18n: I18nService + ) {} private prepareValue(value: any, table?: string, toLower?: boolean) { if (value instanceof Array) { @@ -65,12 +68,8 @@ export class HelperService { } } - public formatReqMessagesString( - i18n: I18nContext, - langTag: string, - vargs: any[] - ) { - const str: any = i18n.t(langTag); + public formatReqMessagesString(langTag: string, vargs: any[]) { + const str: any = this.i18n.t(langTag); const parts: any = str.split("{}"); let insertAt = 1; for (const arg of vargs) { From 322730e7ec81c60ae69931da45dd834539c102f4 Mon Sep 17 00:00:00 2001 From: dhanushkaxep Date: Wed, 1 Mar 2023 10:57:11 +0530 Subject: [PATCH 23/68] issue fixes --- .../Models/UserActionConfirmationModel.tsx | 56 +++++++++++++------ .../Pages/CompanyProfile/companyProfile.tsx | 8 +++ .../Pages/UserManagement/userManagement.tsx | 1 + web/src/Pages/UserProfile/UserProfile.tsx | 4 ++ web/src/Styles/app.scss | 6 +- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/web/src/Components/Models/UserActionConfirmationModel.tsx b/web/src/Components/Models/UserActionConfirmationModel.tsx index 2cc660bfc..8fb20f1cb 100644 --- a/web/src/Components/Models/UserActionConfirmationModel.tsx +++ b/web/src/Components/Models/UserActionConfirmationModel.tsx @@ -1,5 +1,5 @@ -import { Alert, Form, Modal } from 'antd'; -import React, { FC, useState } from 'react'; +import { Alert, Form, Modal, Button } from 'antd'; +import { FC, useEffect, useState } from 'react'; import TextArea from 'antd/lib/input/TextArea'; import { useTranslation } from 'react-i18next'; @@ -9,12 +9,18 @@ export interface UserActionProps { onActionCanceled: any; openModal: any; errorMsg: any; + loading: any; } const UserActionConfirmationModel: FC = (props: UserActionProps) => { - const { actionInfo, onActionConfirmed, onActionCanceled, openModal, errorMsg } = props; + const { actionInfo, onActionConfirmed, onActionCanceled, openModal, errorMsg, loading } = props; const [comment, setComment] = useState(''); const { i18n, t } = useTranslation(['userProfile']); + + useEffect(() => { + setComment(''); + }, [openModal]); + return ( = (props: UserActionProps } className={'popup-' + actionInfo.type} open={openModal} - onOk={() => { - onActionConfirmed(comment); - setComment(''); - }} - onCancel={() => { - onActionCanceled(); - setComment(''); - }} - okText={actionInfo.action} - okButtonProps={{ disabled: comment === '' }} - cancelText={t('userProfile:cancel')} width={Math.min(400, window.innerWidth)} centered={true} destroyOnClose={true} + footer={null} >

{actionInfo.text}

-
+ { + onActionConfirmed(comment); + }} + >