diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb71762235a1187989c66f732efe2aa222b128a2..8396a9ed144b0d4185a8bfd0dd085c5ceac7eade 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,7 @@ test: - export GHOST_HOST_AND_PORT=http://localhost:2368 - export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb - export SALT=$TEST_SALT + - export ELASTICSEARCH_NODE=http://localhost:9200 script: - npm i - npm run test:cov diff --git a/CHANGELOG.md b/CHANGELOG.md index a59f202c1f017b324006555b280e9291176e900f..e1023255dc6824c6d6fdd32e3aef538476ceeae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.8.0](https://forge.grandlyon.com///compare/v1.7.0...v1.8.0) (2021-05-06) + + +### Features + +* add auto-migrate for production ([f0568dc](https://forge.grandlyon.com///commit/f0568dcb3959db84a96c78915a7cd135d163b1b4)) +* add migration for date format ([cae255e](https://forge.grandlyon.com///commit/cae255efe9c174457ae66c94a41e941bf7b2165c)) +* **structures:** add elastic search stack for better search handling ([bbf608b](https://forge.grandlyon.com///commit/bbf608b6e90afaea9ae84ee9121e11ade7dd32c0)) +* add enpoint for couting newsletter ([27cd278](https://forge.grandlyon.com///commit/27cd278781187a3734d06f16331fa9119da7adf4)) +* add new endpoint for structures, data is formated (jointure on modules) ([509f30f](https://forge.grandlyon.com///commit/509f30f9a2c36b18ed093d25fa37c3b9b3849165)) +* add notification mail to admin for structure ([d241ed8](https://forge.grandlyon.com///commit/d241ed8c4597a0151d5a916ae9c0f6aefe0e3092)) + + +### Bug Fixes + +* better naming ([2f837e7](https://forge.grandlyon.com///commit/2f837e78add9bacece2c8a03aa90c828ab7a832f)) +* change freeWorkshop back to notempty ([ad09120](https://forge.grandlyon.com///commit/ad0912014ce7e1da5c94565f4dd05ca14884adb6)) +* changes after review ([b627628](https://forge.grandlyon.com///commit/b62762851345e08d09cde924bc961ecca3123f16)) +* changes on email template (wording...) ([0d18670](https://forge.grandlyon.com///commit/0d18670ca6d889628150d3d04ef1257ef06c3a51)) +* docker-compose missing var + add admin verification on ES index reset ([0af581d](https://forge.grandlyon.com///commit/0af581d8a9d06d0de5b00a65915694cec6d270d2)) +* fix number of results for ES and filters count ([416654e](https://forge.grandlyon.com///commit/416654e6165072314f91d7db3924dd730dbe471b)) +* hide some fiel on endpoint and create migration for opening hours ([ec6d2ac](https://forge.grandlyon.com///commit/ec6d2acfd96a69ffd38e08310ec6f1863332a306)) +* remove mandatory field on structure + update local email sending conf ([7603e47](https://forge.grandlyon.com///commit/7603e47f335aee3ade21ec5d49e7927d7b6eaaa8)) +* remove useless import ([2253581](https://forge.grandlyon.com///commit/2253581b1ebc5a0dd2c0826ed97d9e549bba9402)) +* remove useless improt ([421806d](https://forge.grandlyon.com///commit/421806d13befac5092d87d4864358692938ccfd3)) +* **migration:** fix remove field bug in script ([d6a0f77](https://forge.grandlyon.com///commit/d6a0f775e2cbe6ca9e98d30dc6d136206b587667)) + ## [1.7.0](https://forge.grandlyon.com///compare/v1.6.1...v1.7.0) (2021-04-12) diff --git a/docker-compose.yml b/docker-compose.yml index e95e22ec421be34eadca84b2076cc2b029fd0cb0..94915f69117f8fbfab0d0965a2f7895797b79beb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,10 +8,24 @@ services: - ${SERVICE_API_BIND_PORT}:3000 extra_hosts: - 'sen.grandlyon.com:10.128.16.229' + volumes: + - ./.migrate:/app/.migrate environment: MONGO_NON_ROOT_USERNAME: ${MONGO_NON_ROOT_USERNAME} MONGO_NON_ROOT_PASSWORD: ${MONGO_NON_ROOT_PASSWORD} MONGO_DB_HOST_AND_PORT: ${MONGO_DB_HOST_AND_PORT} + JWT_SECRET: ${JWT_SECRET} + SALT: ${SALT} + MAIL_URL: ${MAIL_URL} + MAIL_TOKEN: ${MAIL_TOKEN} + NODE_ENV: ${NODE_ENV} + APTIC_TOKEN: ${APTIC_TOKEN} + GHOST_HOST_AND_PORT: ${GHOST_HOST_AND_PORT} + GHOST_ADMIN_API_KEY: ${GHOST_ADMIN_API_KEY} + GHOST_CONTENT_API_KEY: ${GHOST_CONTENT_API_KEY} + ELASTICSEARCH_NODE: ${ELASTICSEARCH_NODE} + ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME} + ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD} restart: unless-stopped depends_on: - database-ram @@ -67,6 +81,43 @@ services: volumes: - db-ghost + es01: + image: elasticsearch:7.6.1 + restart: unless-stopped + environment: + node.name: es01 + cluster.name: es-docker-cluster + discovery.type: single-node + xpack.security.enabled: ${ELASTIC_SECURITY} + ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD} + volumes: + - db-elastic + networks: + - elastic + ports: + - ${ELASTICSEARCH_PORT}:9200 + + kib01: + image: docker.elastic.co/kibana/kibana:7.6.1 + restart: unless-stopped + container_name: kib01 + ports: + - ${KIBANA_PORT}:5601 + environment: + ELASTICSEARCH_URL: http://es01:9200 + ELASTICSEARCH_HOSTS: '["http://es01:9200"]' + ELASTICSEARCH_USERNAME: elastic + ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD} + depends_on: + - es01 + networks: + - elastic + volumes: db-ram: db-ghost: + db-elastic: + +networks: + elastic: + driver: bridge diff --git a/package-lock.json b/package-lock.json index d787d4c561026f1861332126e9218bc649e0b94c..7ce909f59ffedfe49c41f1c143c68e1840da7a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ram_server", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -636,6 +636,40 @@ "minimist": "^1.2.0" } }, + "@elastic/elasticsearch": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.12.0.tgz", + "integrity": "sha512-GquUEytCijFRPEk3DKkkDdyhspB3qbucVQOwih9uNyz3iz804I+nGBUsFo2LwVvLQmQfEM0IY2+yoYfEz5wMug==", + "requires": { + "debug": "^4.3.1", + "hpagent": "^0.1.1", + "ms": "^2.1.3", + "pump": "^3.0.0", + "secure-json-parse": "^2.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "@eslint/eslintrc": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", @@ -1356,6 +1390,26 @@ } } }, + "@nestjs/config": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-0.6.3.tgz", + "integrity": "sha512-JxvvUpmH0/WOrTB+zh8dEkxSUQXhB7V3d/qeQXyCnMiEFjaq89+fNFztpWjz4DlOfdS4/eYTzIEy9PH2uGnfzA==", + "requires": { + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "lodash.get": "4.4.2", + "lodash.has": "4.5.2", + "lodash.set": "4.3.2", + "uuid": "8.3.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, "@nestjs/core": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.5.1.tgz", @@ -1370,6 +1424,11 @@ "uuid": "8.3.1" } }, + "@nestjs/elasticsearch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/elasticsearch/-/elasticsearch-7.1.0.tgz", + "integrity": "sha512-3ixmu9MkTh0DS+LKAKcWHLyf/1DPQTXoy+aVClVI14DJQU208oHR3V0e9klApC+GXCYW+BDhNReh4HRyekjTrw==" + }, "@nestjs/jwt": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-7.2.0.tgz", @@ -4921,6 +4980,11 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, "dotgitignore": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz", @@ -5060,7 +5124,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -6941,6 +7004,11 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, + "hpagent": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.1.tgz", + "integrity": "sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ==" + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -8183,6 +8251,7 @@ "version": "12.0.0", "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-12.0.0.tgz", "integrity": "sha512-+8K35LlboWiPuCnXSyiid7rFdxNlpCWWM20WEYe6IZH6psfUWKZmSpSRQ5tk0C0cBeDsvsnIzcef5mYhyJsbug==", + "dev": true, "requires": { "mkdirp": "^1.0.4", "strip-ansi": "^5.2.0", @@ -8193,17 +8262,20 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -8211,7 +8283,8 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -9288,6 +9361,16 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9334,6 +9417,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11158,7 +11246,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11806,6 +11893,11 @@ "ajv-keywords": "^3.4.1" } }, + "secure-json-parse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", + "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -14176,7 +14268,8 @@ "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 79114f2ed061d6b141ac421bafe63a3df12d5d55..0f20477d4039d2e01f2852231f0d219bcf5c5288 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ram_server", "private": true, - "version": "1.7.0", + "version": "1.8.0", "description": "Nest TypeScript starter repository", "license": "MIT", "scripts": { @@ -11,7 +11,7 @@ "start": "ts-node -r tsconfig-paths/register src/main.ts", "start:dev": "nodemon", "start:debug": "nodemon --config nodemon-debug.json", - "start:prod": "node dist/main", + "start:prod": "npm run migrate:up && node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "release": "standard-version", "init-db": "node ./scripts/init-db.js", @@ -25,8 +25,11 @@ "migrate:down": "migrate --migrations-dir=\"./src/migrations/scripts\" --compiler=\"ts:./src/migrations/migrations-utils/ts-compiler.js\" down" }, "dependencies": { + "@elastic/elasticsearch": "^7.12.0", "@nestjs/common": "^7.6.13", + "@nestjs/config": "^0.6.3", "@nestjs/core": "^7.5.1", + "@nestjs/elasticsearch": "^7.1.0", "@nestjs/jwt": "^7.2.0", "@nestjs/mongoose": "^7.1.0", "@nestjs/passport": "^7.1.5", diff --git a/scripts/init-ghost.js b/scripts/init-ghost.js index 350b0376602770cb6b9112cdb8afde728f6a13fe..345ed027d3792a0966ff63452cb05fddeb406fe1 100644 --- a/scripts/init-ghost.js +++ b/scripts/init-ghost.js @@ -81,8 +81,9 @@ function processImagesInHTML(html) { // Find images that Ghost Upload supports let imageRegex = /="([^"]*?(?:\.jpg|\.jpeg|\.gif|\.png|\.svg|\.sgvz))"/gim; let imagePromises = []; + let result; - while ((const result = imageRegex.exec(html)) !== null) { + while ((result = imageRegex.exec(html)) !== null) { let file = result[1]; // Upload the image, using the original matched filename as a reference imagePromises.push( diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index 84dbf424a36ea0a54a4e59cf28ff657f8d546082..dc0bdef307a30973ed3864fa428e46aca34c8dea 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -1,12 +1,14 @@ -import { HttpException, HttpModule, HttpStatus } from '@nestjs/common'; +import { HttpModule } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; import { NewsletterSubscription } from '../newsletter/newsletter-subscription.schema'; import { NewsletterService } from '../newsletter/newsletter.service'; +import { SearchModule } from '../search/search.module'; import { Structure } from '../structures/schemas/structure.schema'; import { StructuresService } from '../structures/services/structures.service'; +import { StructuresSearchService } from '../structures/services/structures-search.service'; import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/users.service'; import { AdminController } from './admin.controller'; @@ -17,10 +19,11 @@ describe('AdminController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigurationModule, HttpModule], + imports: [ConfigurationModule, HttpModule, SearchModule], providers: [ UsersService, StructuresService, + StructuresSearchService, NewsletterService, MailerService, { @@ -47,21 +50,35 @@ describe('AdminController', () => { }); it('should get pending attachments', async () => { - const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}, {name: "Mairie Lyon 7eme", address: "21 boulevard martin"}]; + const result = [ + { name: 'MJC Route de vienne', address: '14 chemin des platanes' }, + { name: 'Mairie Lyon 7eme', address: '21 boulevard martin' }, + ]; jest.spyOn(controller, 'getPendingAttachments').mockImplementation(async (): Promise<any> => result); expect(await controller.getPendingAttachments()).toBe(result); }); it('should validate pending structure', async () => { - const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}]; - const structure: PendingStructureDto = {userEmail:"martin@mjc.fr", structureId: "1", structureName:"MJC Route de vienne"}; + const result = [{ name: 'MJC Route de vienne', address: '14 chemin des platanes' }]; + const structure: PendingStructureDto = { + userEmail: 'martin@mjc.fr', + structureId: '1', + structureName: 'MJC Route de vienne', + }; jest.spyOn(controller, 'validatePendingStructure').mockImplementation(async (): Promise<any> => result); expect(await controller.validatePendingStructure(structure)).toBe(result); }); it('should refuse pending structure', async () => { - const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}, {name: "Mairie Lyon 7eme", address: "21 boulevard martin"}]; - const structure: PendingStructureDto = {userEmail:"martin@mjc.fr", structureId: "1", structureName:"MJC Route de vienne"}; + const result = [ + { name: 'MJC Route de vienne', address: '14 chemin des platanes' }, + { name: 'Mairie Lyon 7eme', address: '21 boulevard martin' }, + ]; + const structure: PendingStructureDto = { + userEmail: 'martin@mjc.fr', + structureId: '1', + structureName: 'MJC Route de vienne', + }; jest.spyOn(controller, 'refusePendingStructure').mockImplementation(async (): Promise<any> => result); expect(await controller.refusePendingStructure(structure)).toBe(result); }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 03442f8e55e2210e3bf1fc52f96e0c4617ff3add..999c6d949ba0026bdc654d172dc703459030ed50 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Delete, Param } from '@nestjs/common'; import { Controller, Get, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiParam } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { NewsletterSubscriptionDocument } from '../newsletter/newsletter-subscription.schema'; import { NewsletterService } from '../newsletter/newsletter.service'; import { StructuresService } from '../structures/services/structures.service'; import { Roles } from '../users/decorators/roles.decorator'; @@ -111,6 +112,13 @@ export class AdminController { else return this.newsletterService.findAll(); } + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Get('countNewsletterSubscriptions') + public async countNewsletterSubscriptions(): Promise<number> { + return this.newsletterService.countNewsletterSubscriptions(); + } + @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') @Delete('newsletterSubscription/:email') diff --git a/src/app.module.ts b/src/app.module.ts index ed6a03004e4e90edc2decf97c41b7f3478ca4f69..3cd15b2b24ef294009ddb6c3a55989789bd9bfd4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,7 +29,7 @@ import { NewsletterModule } from './newsletter/newsletter.module'; AdminModule, PostsModule, TempUserModule, - NewsletterModule + NewsletterModule, ], controllers: [AppController], }) diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts index bc6f3c86841008cb9bed60f0c70bbd517b947403..3373d355f83e81b4d19fa7ebcf1f2ca1b4487c87 100644 --- a/src/categories/categories.module.ts +++ b/src/categories/categories.module.ts @@ -19,7 +19,10 @@ import { CategoriesAccompagnement, CategoriesAccompagnementSchema } from './sche ]), ], controllers: [CategoriesFormationsController, CategoriesAccompagnementController, CategoriesOthersController], - exports: [CategoriesFormationsService], + exports: [CategoriesFormationsService, CategoriesAccompagnementService, CategoriesOthersService], providers: [CategoriesFormationsService, CategoriesAccompagnementService, CategoriesOthersService], }) -export class CategoriesModule {} +export class CategoriesModule { + id: string; + text: any; +} diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 0506dc2ae8a5b211ad5883852d50858f71e84fda..97ab675e89c54d4e224c3ebec86ed372bbb45ce9 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -4,9 +4,9 @@ export const config = { host: 'localhost', protocol: 'http', port: '4200', - from: 'inclusionnumerique@grandlyon.com', + from: 'noreplyinclusionnumerique@grandlyon.com', from_name: 'Réseau des acteurs de la médiation numérique', - replyTo: 'inclusionnumerique@grandlyon.com', + replyTo: 'noreplyinclusionnumerique@grandlyon.com', templates: { directory: './src/mailer/mail-templates', verify: { @@ -53,5 +53,13 @@ export const config = { ejs: 'structureErrorReport.ejs', json: 'structureErrorReport.json', }, + structureModificationNotification: { + ejs: 'structureModificationNotification.ejs', + json: 'structureModificationNotification.json', + }, + structureDeletionNotification: { + ejs: 'structureDeletionNotification.ejs', + json: 'structureDeletionNotification.json', + }, }, }; diff --git a/src/mailer/mail-templates/adminStructureClaim.ejs b/src/mailer/mail-templates/adminStructureClaim.ejs index 8eff0993341c0655469c5cd2dfbe1aa50a9180ff..1e7e162f39e671fdf5d6beb2e1765a7d284e37e5 100644 --- a/src/mailer/mail-templates/adminStructureClaim.ejs +++ b/src/mailer/mail-templates/adminStructureClaim.ejs @@ -1,4 +1,4 @@ -Bonjour<br /> +Bonjour,<br /> <br /> Une nouvelle structure a été revendiquée. Pour valider ou refuser la demande, merci de vous rendre sur <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/admin">ce lien</a>. diff --git a/src/mailer/mail-templates/adminStructureCreate.ejs b/src/mailer/mail-templates/adminStructureCreate.ejs index 47b79daa15b999739415094be15f42e9f641ede0..0fa351673b20ec097f09119ed1aefb97c9746382 100644 --- a/src/mailer/mail-templates/adminStructureCreate.ejs +++ b/src/mailer/mail-templates/adminStructureCreate.ejs @@ -1,8 +1,8 @@ -Bonjour<br /> +Bonjour,<br /> <br /> -Une nouvelle structure a été créé: +Une nouvelle structure a été créée: <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" - ><strong><%= name %></strong></a + ><strong><%= structureName %></strong></a > <br /> Il est possible que la structure ne soit pas immédiatement visible sur la carto. L'utilisateur doit valider son compte diff --git a/src/mailer/mail-templates/apticStructureDuplication.ejs b/src/mailer/mail-templates/apticStructureDuplication.ejs index 53d99ec48ecccb22849a867d2579fac919ec78f9..0b30ee72bff6faf595b43702914c0e7bb204b269 100644 --- a/src/mailer/mail-templates/apticStructureDuplication.ejs +++ b/src/mailer/mail-templates/apticStructureDuplication.ejs @@ -1,4 +1,4 @@ Bonjour,<br /> <br /> La fiche structure: <strong><%= name %></strong> a été créée après récupération des données aptic. Elle correspond -potientiellement a la structure existante : <strong><%= duplicatedStructureName %></strong>. +potientiellement à la structure existante : <strong><%= duplicatedStructureName %></strong>. diff --git a/src/mailer/mail-templates/resetPassword.ejs b/src/mailer/mail-templates/resetPassword.ejs index cf29cee55cd7ee9881479fef67ac49eaf27d1a7d..233966c0e062c056cb73c5afcbdd2461448c941b 100644 --- a/src/mailer/mail-templates/resetPassword.ejs +++ b/src/mailer/mail-templates/resetPassword.ejs @@ -1,4 +1,4 @@ -Bonjour<br /> +Bonjour,<br /> <br /> Vous avez demandé une réinitialisation de votre mot de passe pour le <em>Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon</em>. Pour changer de mot de passe, merci de @@ -7,4 +7,4 @@ cliquer sur le lien suivant : href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/reset-password?token=<%= token %>" >ce lien</a ><br /> -Si vous n'avez pas demander de réinitiallisation de votre mot de passe, merci d'ignorer cet email. +Si vous n'avez pas demandé de réinitiallisation de votre mot de passe, merci d'ignorer cet email. diff --git a/src/mailer/mail-templates/structureClaimValidation.ejs b/src/mailer/mail-templates/structureClaimValidation.ejs index 9e04b8973bf0a6fea16782f07e4537bf65369314..63622c87def5631e1568e59dd550d4cb969b8c88 100644 --- a/src/mailer/mail-templates/structureClaimValidation.ejs +++ b/src/mailer/mail-templates/structureClaimValidation.ejs @@ -1,11 +1,11 @@ -Bonjour<br /> +Bonjour,<br /> <br /> -La demande de rattachement de votre compte a la structure <strong><%= name %></strong> a été +La demande de rattachement de votre compte à la structure <strong><%= name %></strong> a été <strong><%= status %></strong>. <%if (status === 'refusée') { %> <p> - Vous considérer qu’une erreur a été commise, vous pouvez les contacter les administrateurs à l’adresse - <a href="mailto:inclusionnumerique@grandlyon.com">inclusionnumerique@grandlyon.com</a> + Vous considérez qu’une erreur a été commise, vous pouvez contacter les administrateurs à l’adresse + <a href="mailto:inclusionnumerique@grandlyon.com">inclusionnumerique@grandlyon.com</a>. </p> <% } else{ %> -<p>Vous pouvez dorénavant mettre à jour la fiche de votre structure</p> +<p>Vous pouvez dorénavant mettre à jour la fiche de votre structure.</p> <% } %> diff --git a/src/mailer/mail-templates/structureDeletionNotification.ejs b/src/mailer/mail-templates/structureDeletionNotification.ejs new file mode 100644 index 0000000000000000000000000000000000000000..0ea05b23462a8b189f4e73d923945c27f165fa94 --- /dev/null +++ b/src/mailer/mail-templates/structureDeletionNotification.ejs @@ -0,0 +1,3 @@ +Bonjour,<br /> +<br /> +Un utilisateur a supprimé la fiche de sa structure (<%= structureName %>). diff --git a/src/mailer/mail-templates/structureDeletionNotification.json b/src/mailer/mail-templates/structureDeletionNotification.json new file mode 100644 index 0000000000000000000000000000000000000000..3fabb0edbd05f73e363f5f035dafd55614d4a33f --- /dev/null +++ b/src/mailer/mail-templates/structureDeletionNotification.json @@ -0,0 +1,3 @@ +{ + "subject": "Une structure à été supprimé de Res'in, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/mailer/mail-templates/structureErrorReport.ejs b/src/mailer/mail-templates/structureErrorReport.ejs index 829029e95f157b6034c60889e7ee0798f2518d48..18b32cd0ab99a7fcf5246656f1b19bd2a4121727 100644 --- a/src/mailer/mail-templates/structureErrorReport.ejs +++ b/src/mailer/mail-templates/structureErrorReport.ejs @@ -1,4 +1,4 @@ -Bonjour<br /> +Bonjour,<br /> <br /> Un utilisateur de Res'in a relevé une erreur sur la fiche de votre structure (<%= structureName %>). <br /> diff --git a/src/mailer/mail-templates/structureJoinRequest.ejs b/src/mailer/mail-templates/structureJoinRequest.ejs index e668707556f5e096aa2a84028bbf179612091f52..cdfbb3c341198a71bc97947a5d59aaef20e0cdf9 100644 --- a/src/mailer/mail-templates/structureJoinRequest.ejs +++ b/src/mailer/mail-templates/structureJoinRequest.ejs @@ -1,6 +1,6 @@ -Bonjour<br /> +Bonjour,<br /> <br /> -Vous recevez ce message car <strong><%= surname %></strong> <strong><%= name %></strong> demande a rejoindre votre +Vous recevez ce message car <strong><%= surname %></strong> <strong><%= name %></strong> demande à rejoindre votre stucture <strong><%= structureName %></strong> sur RES'in, le réseau des acteurs de l'inclusion numérique de la Métropole de Lyon. Vous pouvez dès maintenant valider la demande en <a diff --git a/src/mailer/mail-templates/structureModificationNotification.ejs b/src/mailer/mail-templates/structureModificationNotification.ejs new file mode 100644 index 0000000000000000000000000000000000000000..6a055cd4595dc21c7546a30d0a0f791bb5b41f1a --- /dev/null +++ b/src/mailer/mail-templates/structureModificationNotification.ejs @@ -0,0 +1,7 @@ +Bonjour,<br /> +<br /> +Un utilisateur a modifié une ou plusieurs informations sur la fiche de sa structure (<%= structureName %>). +<br /> +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" + >Acceder à cette structure</a +>. diff --git a/src/mailer/mail-templates/structureModificationNotification.json b/src/mailer/mail-templates/structureModificationNotification.json new file mode 100644 index 0000000000000000000000000000000000000000..516a30844c6f178ee77134da581d1b44786b6548 --- /dev/null +++ b/src/mailer/mail-templates/structureModificationNotification.json @@ -0,0 +1,3 @@ +{ + "subject": "Une fiche de structure à été mise à jour, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/mailer/mail-templates/structureOutdatedInfo.ejs b/src/mailer/mail-templates/structureOutdatedInfo.ejs index 5427f8b22825c64ab25c3da4d903b3dc27742cbd..a5caf60e0c7cff65131144da363bfb58bcfecc38 100644 --- a/src/mailer/mail-templates/structureOutdatedInfo.ejs +++ b/src/mailer/mail-templates/structureOutdatedInfo.ejs @@ -1,6 +1,6 @@ -Bonjour<br /> +Bonjour,<br /> <br /> -Vous recevez ce message, parce que votre structure <strong><%= name %></strong> est référencée sur RES'in, le réseau des +Vous recevez ce message car votre structure <strong><%= name %></strong> est référencée sur RES'in, le réseau des acteurs de l'inclusion numérique de la Métropole de Lyon. Pouvez-vous nous aider en vérifiant que vos données sont bien à jour en <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" diff --git a/src/mailer/mail-templates/tempUserRegistration.ejs b/src/mailer/mail-templates/tempUserRegistration.ejs index fb336b0625aa83309199cd5f791f6f4037db87d4..59e46a0b6678dae30977d9ca44dd514052a00526 100644 --- a/src/mailer/mail-templates/tempUserRegistration.ejs +++ b/src/mailer/mail-templates/tempUserRegistration.ejs @@ -1,6 +1,6 @@ -Bonjour<br /> +Bonjour,<br /> <br /> -Vous recevez ce message car vous avez été relié a la stucture <strong><%= name %></strong> sur RES'in, le réseau des +Vous recevez ce message car vous avez été relié à la stucture <strong><%= name %></strong> sur RES'in, le réseau des acteurs de l'inclusion numérique de la Métropole de Lyon. Vous pouvez dès maitenant vous créer un compte sur la plateforme pour accéder a votre structure en <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/register?id=<%= id %>" diff --git a/src/mailer/mail-templates/verify.ejs b/src/mailer/mail-templates/verify.ejs index 4f2780e183b6d598a6c94d5dc8ad463abe9bf06f..c2c57e73b624c9915da4af8680e1a3bcb4afba82 100644 --- a/src/mailer/mail-templates/verify.ejs +++ b/src/mailer/mail-templates/verify.ejs @@ -1,8 +1,8 @@ -Bonjour<br /> +Bonjour,<br /> <br /> Afin de pouvoir vous connecter sur la plateforme, merci de cliquer sur <a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/users/verify/<%= userId %>?token=<%= token %>" >ce lien</a > -afin de valider votre inscription<br /> +pour valider votre inscription.<br /> diff --git a/src/migrations/scripts/1617962328658-add-newsletter-data.ts b/src/migrations/scripts/1617962328658-add-newsletter-data.ts deleted file mode 100644 index 17821b927c47a2d58fb3544f34b98917d7f465e4..0000000000000000000000000000000000000000 --- a/src/migrations/scripts/1617962328658-add-newsletter-data.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Db } from 'mongodb'; -import { getDb } from '../migrations-utils/db'; -import * as fs from 'fs'; - -export const up = async () => { - const db: Db = await getDb(); - const data = fs.readFileSync('/app/src/migrations/data/newsletter-data.json', 'utf8'); - const parsedData = JSON.parse(data); - db.collection('newslettersubscriptions').insertMany(parsedData); -}; - -export const down = async () => { - const db: Db = await getDb(); - /* - Code you downgrade script here! - */ -}; diff --git a/src/migrations/scripts/1618322296327-clean-data.ts b/src/migrations/scripts/1618322296327-clean-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..03afdd82c811aefcbae834a4f84a3ed7e083a590 --- /dev/null +++ b/src/migrations/scripts/1618322296327-clean-data.ts @@ -0,0 +1,90 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; + +export const up = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = updateStructure(document); + await db + .collection('structures') + .updateOne({ _id: document._id }, [ + { $set: newDoc }, + { $unset: ['equipmentsDetails', 'nomDeLusager', 'statutJuridique', 'documentsMeeting'] }, + ]); + } + console.log(`Update done`); +}; + +export const down = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = downgradeStructure(document); + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +function updateStructure(doc) { + return updateHours(doc); +} + +function downgradeStructure(doc) { + doc = restoreHours(doc); + return doc; +} + +function updateHours(doc) { + if (doc.hours) { + Object.keys(doc.hours).forEach((key) => { + if (doc.hours[key].time.length > 0) { + doc.hours[key].time.forEach((timeRange) => { + timeRange.openning = formatHours(timeRange.openning); + timeRange.closing = formatHours(timeRange.closing); + }); + } + }); + return doc; + } else { + console.warn(`No hours on doc ${doc._id}`); + return doc; + } +} + +function restoreHours(doc) { + if (doc.hours) { + Object.keys(doc.hours).forEach((key) => { + if (doc.hours[key].time.length > 0) { + doc.hours[key].time.forEach((timeRange) => { + timeRange.openning = formatBackHours(timeRange.openning); + timeRange.closing = formatBackHours(timeRange.closing); + }); + } + }); + return doc; + } else { + console.warn(`No hours on doc ${doc._id}`); + return doc; + } +} + +function formatHours(hour): string { + const stringifiedHour = hour.toString(); + if (stringifiedHour.length === 3) { + // 930 + return stringifiedHour.slice(0, 1) + ':' + stringifiedHour.slice(1, 3); + } else if (stringifiedHour.length === 4) { + // 1200 + return stringifiedHour.slice(0, 2) + ':' + stringifiedHour.slice(2, 4); + } +} + +function formatBackHours(hour): number { + const splitedHour = hour.split(':'); + return parseInt(''.concat(...splitedHour), 10); +} diff --git a/src/migrations/scripts/1620229047628-opening-hours.ts b/src/migrations/scripts/1620229047628-opening-hours.ts new file mode 100644 index 0000000000000000000000000000000000000000..69c65461eaf66c60ef1444c3eeb40b154c57fb3a --- /dev/null +++ b/src/migrations/scripts/1620229047628-opening-hours.ts @@ -0,0 +1,66 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; + +export const up = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = updateStructure(document); + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +export const down = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = downgradeStructure(document); + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +function updateStructure(doc) { + return updateHours(doc); +} + +function downgradeStructure(doc) { + return restoreHours(doc); +} + +function updateHours(doc) { + if (doc.hours) { + Object.keys(doc.hours).forEach((key) => { + doc.hours[key].time.forEach((timeRange) => { + timeRange.opening = timeRange.openning; + delete timeRange.openning; + }); + }); + return doc; + } else { + console.warn(`No hours on doc ${doc._id}`); + return doc; + } +} + +function restoreHours(doc) { + if (doc.hours) { + Object.keys(doc.hours).forEach((key) => { + if (doc.hours[key].time.length > 0) { + doc.hours[key].time.forEach((timeRange) => { + timeRange.openning = timeRange.opening; + delete timeRange.opening; + }); + } + }); + return doc; + } else { + console.warn(`No hours on doc ${doc._id}`); + return doc; + } +} diff --git a/src/migrations/scripts/1620289895495-timestamp-format.ts b/src/migrations/scripts/1620289895495-timestamp-format.ts new file mode 100644 index 0000000000000000000000000000000000000000..1abbf94695e8f3f0280bb98d2f31754383ef668f --- /dev/null +++ b/src/migrations/scripts/1620289895495-timestamp-format.ts @@ -0,0 +1,46 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; + +export const up = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = updateStructure(document); + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +export const down = async () => { + const db: Db = await getDb(); + + const cursor = db.collection('structures').find({}); + let document; + while ((document = await cursor.next())) { + const newDoc = downgradeStructure(document); + await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]); + } + console.log(`Update done`); +}; + +function updateStructure(doc) { + return updateTimestamp(doc); +} + +function downgradeStructure(doc) { + return restoreTimestamp(doc); +} + +function updateTimestamp(doc) { + doc.createdAt = new Date(doc.createdAt).toISOString(); + doc.updatedAt = new Date(doc.updatedAt).toISOString(); + return doc; +} + +function restoreTimestamp(doc) { + doc.createdAt = new Date(doc.createdAt).toString(); + doc.updatedAt = new Date(doc.updatedAt).toISOString(); + return doc; +} diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts index c1ca5efc0e07368586b7a3ac99c7248c43d8a85f..57dab32d153c9d0843eca5e005a49da16123853f 100644 --- a/src/newsletter/newsletter.service.ts +++ b/src/newsletter/newsletter.service.ts @@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; -import { NewsletterSubscription } from './newsletter-subscription.schema'; +import { NewsletterSubscription, NewsletterSubscriptionDocument } from './newsletter-subscription.schema'; @Injectable() export class NewsletterService { @@ -32,10 +32,14 @@ export class NewsletterService { return this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); } - public async searchNewsletterSubscription(searchString: string) { + public async searchNewsletterSubscription(searchString: string): Promise<NewsletterSubscriptionDocument[]> { return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') }).exec(); } + public async countNewsletterSubscriptions(): Promise<number> { + return this.newsletterSubscriptionModel.countDocuments({}).exec(); + } + public async deleteOneEmail(mail: string): Promise<NewsletterSubscription | undefined> { const subscription = await this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); if (!subscription) { diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e9b5e85b2226d75c157886fe06eabf64c265d7b --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; + +@Module({ + imports: [ + ConfigModule, + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: configService.get('ELASTICSEARCH_NODE'), + auth: { + username: configService.get('ELASTICSEARCH_USERNAME'), + password: configService.get('ELASTICSEARCH_PASSWORD'), + }, + }), + inject: [ConfigService], + }), + ], + exports: [ElasticsearchModule], +}) +export class SearchModule {} diff --git a/src/structures/interfaces/structure-search-body.interface.ts b/src/structures/interfaces/structure-search-body.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..24d7591930d82643cd181ced7132665d8d2007d9 --- /dev/null +++ b/src/structures/interfaces/structure-search-body.interface.ts @@ -0,0 +1,8 @@ +import { Address } from '../schemas/address.schema'; + +export interface StructureSearchBody { + structureId: string; + structureName: string; + structureType: string; + address: Address; +} diff --git a/src/structures/interfaces/structure-search-response.interface.ts b/src/structures/interfaces/structure-search-response.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..7497343827756c4a16df4e15a861709604a00ce0 --- /dev/null +++ b/src/structures/interfaces/structure-search-response.interface.ts @@ -0,0 +1,10 @@ +import { StructureSearchBody } from './structure-search-body.interface'; + +export interface StructureSearchResult { + hits: { + total: number; + hits: Array<{ + _source: StructureSearchBody; + }>; + }; +} diff --git a/src/structures/schemas/time.schema.ts b/src/structures/schemas/time.schema.ts index 986b7773a38ace7f052d28519321bbb2e37efeab..8a2261331041add1791775b9da1c2e1ee323a708 100644 --- a/src/structures/schemas/time.schema.ts +++ b/src/structures/schemas/time.schema.ts @@ -4,8 +4,8 @@ import { Document } from 'mongoose'; export type TimeDocument = Time & Document; export class Time { - openning: number; - closing: number; + opening: string; + closing: string; } export const TimeSchema = SchemaFactory.createForClass(Time); diff --git a/src/structures/services/structures-search.service.ts b/src/structures/services/structures-search.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e809c58f9f53f9e0d1291df3ee28c7f163cb212c --- /dev/null +++ b/src/structures/services/structures-search.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { structureDto } from '../dto/structure.dto'; +import { StructureDocument } from '../schemas/structure.schema'; +import { StructureSearchBody } from '../interfaces/structure-search-body.interface'; +import { StructureSearchResult } from '../interfaces/structure-search-response.interface'; + +@Injectable() +export class StructuresSearchService { + private index = 'structures'; + + constructor(private readonly elasticsearchService: ElasticsearchService) {} + + public async indexStructure(structure: StructureDocument): Promise<StructureDocument> { + this.elasticsearchService.index<StructureSearchResult, StructureSearchBody>({ + index: this.index, + id: structure._id, + body: { + structureName: structure.structureName, + structureType: structure.structureType, + structureId: structure._id, + address: structure.address, + }, + }); + return structure; + } + + public async createStructureIndex(): Promise<any> { + return await this.elasticsearchService.indices.create({ + index: this.index, + }); + } + + public async dropIndex(): Promise<any> { + if ( + ( + await this.elasticsearchService.indices.exists({ + index: this.index, + }) + ).body + ) { + return this.elasticsearchService.indices.delete({ + index: this.index, + }); + } + } + + public async deleteIndexStructure(structure: StructureDocument): Promise<StructureDocument> { + this.elasticsearchService.delete<StructureSearchResult, StructureSearchBody>({ + index: this.index, + id: structure._id, + }); + return structure; + } + + public async search(searchString: string): Promise<StructureSearchBody[]> { + searchString = searchString ? searchString + '*' : '*'; + const { body } = await this.elasticsearchService.search<StructureSearchResult>({ + index: this.index, + body: { + from: 0, + size: 200, + query: { + query_string: { + analyze_wildcard: 'true', + query: searchString, + fields: ['structureName^5', 'structureType^5', 'address.street', 'address.commune^5'], + fuzziness: 'AUTO', + }, + }, + }, + }); + const hits = body.hits.hits; + return hits.map((item) => item._source); + } + + public async update(structure: structureDto, id: string): Promise<any> { + return this.elasticsearchService.update({ + index: this.index, + id: id, + body: { + doc: { + structureName: structure.structureName, + structureType: structure.structureType, + structureId: id, + address: structure.address, + }, + }, + }); + } +} diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 71b6bb189b6aef32dcf401e1bfcdcebe08febdb9..aec2207c635d21b40560bf7df44019fae8f76c5c 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -14,6 +14,12 @@ import { DateTime } from 'luxon'; import { IUser } from '../../users/interfaces/user.interface'; import * as _ from 'lodash'; import { OwnerDto } from '../../users/dto/owner.dto'; +import { StructuresSearchService } from './structures-search.service'; +import { CategoriesFormationsModule } from '../../categories/schemas/categoriesFormationsModule.schema'; +import { CategoriesModule } from '../../categories/categories.module'; +import { CategoriesAccompagnement } from '../../categories/schemas/categoriesAccompagnement.schema'; +import { CategoriesFormations } from '../../categories/schemas/categoriesFormations.schema'; +import { CategoriesOthers } from '../../categories/schemas/categoriesOthers.schema'; @Injectable() export class StructuresService { @@ -21,9 +27,44 @@ export class StructuresService { private readonly httpService: HttpService, private readonly userService: UsersService, private readonly mailerService: MailerService, + private structuresSearchService: StructuresSearchService, @InjectModel(Structure.name) private structureModel: Model<StructureDocument> ) {} + async initiateStructureIndex(): Promise<StructureDocument[]> { + await this.structuresSearchService.dropIndex(); + await this.structuresSearchService.createStructureIndex(); + return this.populateES(); + } + + async searchForStructures(text: string, filters?: Array<any>): Promise<StructureDocument[]> { + const results = await this.structuresSearchService.search(text); + const ids = results.map((result) => result.structureId); + if (!ids.length) { + return []; + } + //we match ids from Elasticsearch with ids from mongoDB (and filters) and sort the result according to ElasticSearch order. + if (filters.length > 0) { + return ( + await this.structureModel + .find({ + _id: { $in: ids }, + $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], + }) + .exec() + ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + } else { + return ( + await this.structureModel + .find({ + _id: { $in: ids }, + $and: [{ deletedAt: { $exists: false }, accountVerified: true }], + }) + .exec() + ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + } + } + public async create(idUser: string, structure: structureDto): Promise<Structure> { const user = await this.userService.findOne(idUser); if (!user) { @@ -32,16 +73,25 @@ export class StructuresService { const createdStructure = new this.structureModel(structure); createdStructure._id = Types.ObjectId(); await createdStructure.save(); - await this.getStructurePosition(createdStructure).then((postition: StructureDocument) => { - return this.structureModel - .findByIdAndUpdate(Types.ObjectId(createdStructure._id), { address: postition.address, coord: postition.coord }) - .exec(); + await this.getStructurePosition(createdStructure).then(async (postition: StructureDocument) => { + return this.structuresSearchService.indexStructure( + await this.structureModel + .findByIdAndUpdate(Types.ObjectId(createdStructure._id), { + address: postition.address, + coord: postition.coord, + }) + .exec() + ); }); user.structuresLink.push(createdStructure._id); user.save(); // Senc admin notification mail - this.userService.sendAdminNewStructureMail(createdStructure.structureName, createdStructure._id); + this.sendAdminStructureNotification( + createdStructure, + this.mailerService.config.templates.adminStructureCreate.ejs, + this.mailerService.config.templates.adminStructureCreate.json + ); return createdStructure; } @@ -102,6 +152,111 @@ export class StructuresService { return this.structureModel.find({ deletedAt: { $exists: false }, accountVerified: true }).exec(); } + public async populateES(): Promise<StructureDocument[]> { + const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + await Promise.all( + structures.map((structure: StructureDocument) => { + this.structuresSearchService.indexStructure(structure); + }) + ); + return structures; + } + + public async findAllFormated( + formationCategories: CategoriesFormations[], + accompagnementCategories: CategoriesAccompagnement[], + otherCategories: CategoriesOthers[] + ): Promise<StructureDocument[]> { + const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + + // Update structures coord and address before sending them + await Promise.all( + structures.map((structure: StructureDocument) => { + // If structre has no address, add it + if (!structure.address || structure.coord.length <= 0) { + return this.getStructurePosition(structure).then((postition: StructureDocument) => { + this.structureModel + .findByIdAndUpdate(Types.ObjectId(structure._id), { + address: postition.address, + coord: postition.coord, + }) + .exec(); + }); + } + }) + ); + return ( + await this.structureModel + .find({ deletedAt: { $exists: false }, accountVerified: true }) + .select('-_id -accountVerified -otherDescription') + .exec() + ).map((structure) => { + structure.proceduresAccompaniment = this.mapModules( + structure.proceduresAccompaniment, + accompagnementCategories.find((category) => category.name === 'Accompagnement des démarches').modules + ); + structure.labelsQualifications = this.mapModules( + structure.labelsQualifications, + otherCategories.find((category) => category.name === 'Labels et qualifications').modules + ); + structure.publics = this.mapModules( + structure.publics, + otherCategories.find((category) => category.name === 'Publics acceptés').modules + ); + structure.accessModality = this.mapModules( + structure.accessModality, + otherCategories.find((category) => category.name === "Modalités d'accès").modules + ); + structure.publicsAccompaniment = this.mapModules( + structure.publicsAccompaniment, + otherCategories.find((category) => category.name === 'Accompagnement des publics spécifique').modules + ); + structure.equipmentsAndServices = this.mapModules( + structure.equipmentsAndServices, + otherCategories.find((category) => category.name === 'Équipements et services proposés').modules + ); + structure.baseSkills = this.mapFormationModules( + structure.baseSkills, + formationCategories.find((category) => category.name === 'Les compétences de base').modules + ); + structure.accessRight = this.mapFormationModules( + structure.accessRight, + formationCategories.find((category) => category.name === 'Accès aux droits').modules + ); + structure.socialAndProfessional = this.mapFormationModules( + structure.socialAndProfessional, + formationCategories.find((category) => category.name === 'Insertion sociale et professionnelle').modules + ); + structure.parentingHelp = this.mapFormationModules( + structure.parentingHelp, + formationCategories.find((category) => category.name === 'Aide à la parentalité').modules + ); + structure.digitalCultureSecurity = this.mapFormationModules( + structure.digitalCultureSecurity, + formationCategories.find((category) => category.name === 'Culture et sécurité numérique').modules + ); + return structure; + }); + } + public mapFormationModules(structureModule: string[], baseModule: CategoriesFormationsModule[]): string[] { + if (structureModule == []) { + return []; + } + return structureModule.map((id) => { + const formatedText = baseModule.find((module) => module.display_id === id)?.text; + return formatedText ? formatedText : id; + }); + } + public mapModules(structureModule: string[], baseModule: CategoriesModule[]): string[] { + if (structureModule == []) { + return []; + } + return structureModule.map((id) => { + const formatedText = baseModule.find((module) => module.id === id)?.text; + return formatedText ? formatedText : id; + }); + } + public async update(idStructure: string, structure: structureDto): Promise<Structure> { const oldStructure = await this.findOne(idStructure); if (!_.isEqual(oldStructure.address, structure.address)) { @@ -111,6 +266,12 @@ export class StructuresService { if (!result) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); } else { + await this.structuresSearchService.update(structure, idStructure); + this.sendAdminStructureNotification( + result, + this.mailerService.config.templates.structureModificationNotification.ejs, + this.mailerService.config.templates.structureModificationNotification.json + ); this.userService.removeOutdatedStructureFromArray(idStructure); } return this.findOne(idStructure); @@ -195,12 +356,14 @@ export class StructuresService { keyList.push({ [key]: { $elemMatch: { $eq: value } }, deletedAt: { $exists: false }, + accountVerified: true, }); if (selected && selected.length > 0) { for (const val of selected) { keyList.push({ [val.text]: { $elemMatch: { $eq: val.id } }, deletedAt: { $exists: false }, + accountVerified: true, }); } } @@ -228,14 +391,43 @@ export class StructuresService { if (!structure) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); } + this.structuresSearchService.deleteIndexStructure(structure); structure.structureType = null; structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString(); this.anonymizeStructure(structure).save(); // Remove structure from userModel this.userService.removeStructureIdFromUsers(structure._id); + this.sendAdminStructureNotification( + structure, + this.mailerService.config.templates.structureDeletionNotification.ejs, + this.mailerService.config.templates.structureDeletionNotification.json + ); return structure; } + public async sendAdminStructureNotification( + structure: StructureDocument, + templateLocation: any, + jsonConfigLocation: any + ) { + const uniqueAdminEmails = [...new Set((await this.userService.getAdmins()).map((admin) => admin.email))].map( + (item) => { + return { email: item }; + } + ); + + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(templateLocation); + const jsonConfig = this.mailerService.loadJsonConfig(jsonConfigLocation); + + const html = await ejs.renderFile(ejsPath, { + config, + id: structure ? structure._id : 0, + structureName: structure ? structure.structureName : '', + }); + this.mailerService.send(uniqueAdminEmails, jsonConfig.subject, html); + } + private anonymizeStructure(structure: StructureDocument): StructureDocument { structure.contactPhone = ''; structure.contactMail = ''; diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 67e12c9a15469d46919601363dbfbe8882344fce..65dcfd00b0b4d5495ce84306e940f99a82982981 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -15,6 +15,9 @@ import { import { ApiParam } from '@nestjs/swagger'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { CategoriesAccompagnementService } from '../categories/services/categories-accompagnement.service'; +import { CategoriesFormationsService } from '../categories/services/categories-formations.service'; +import { CategoriesOthersService } from '../categories/services/categories-others.service'; import { CreateTempUserDto } from '../temp-user/dto/create-temp-user.dto'; import { TempUserService } from '../temp-user/temp-user.service'; import { Roles } from '../users/decorators/roles.decorator'; @@ -24,8 +27,9 @@ import { UsersService } from '../users/users.service'; import { CreateStructureDto } from './dto/create-structure.dto'; import { QueryStructure } from './dto/query-structure.dto'; import { structureDto } from './dto/structure.dto'; -import { Structure } from './schemas/structure.schema'; +import { Structure, StructureDocument } from './schemas/structure.schema'; import { StructuresService } from './services/structures.service'; +import { RolesGuard } from '../users/guards/roles.guard'; @Controller('structures') export class StructuresController { @@ -33,7 +37,10 @@ export class StructuresController { private readonly httpService: HttpService, private readonly structureService: StructuresService, private readonly userService: UsersService, - private readonly tempUserService: TempUserService + private readonly tempUserService: TempUserService, + private readonly categoriesFormationsService: CategoriesFormationsService, + private readonly categoriesOthersService: CategoriesOthersService, + private readonly categoriesAccompagnementService: CategoriesAccompagnementService ) {} /** @@ -59,7 +66,14 @@ export class StructuresController { @Post('search') public async search(@Query() query: QueryStructure, @Body() body): Promise<Structure[]> { - return this.structureService.search(query.query, body ? body.filters : null); + return await this.structureService.searchForStructures(query.query, body ? body.filters : null); + } + + @Post('resetSearchIndex') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + public async resetES(): Promise<StructureDocument[]> { + return this.structureService.initiateStructureIndex(); } @Put('updateAfterOwnerVerify/:id') @@ -79,6 +93,14 @@ export class StructuresController { return this.structureService.findAll(); } + @Get('formated') + public async findAllFormated(): Promise<Structure[]> { + const formationCategories = await this.categoriesFormationsService.findAll(); + const accompagnementCategories = await this.categoriesAccompagnementService.findAll(); + const otherCategories = await this.categoriesOthersService.findAll(); + return this.structureService.findAllFormated(formationCategories, accompagnementCategories, otherCategories); + } + @Post(':id/isClaimed') public async isClaimed(@Param('id') id: string, @Body() user?: User): Promise<boolean> { return this.structureService.isClaimed(id, user); diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 1445999d289ff826e1504e3754d814f10ed06322..650e3adfd9b7ef57e13d8e06090f1b9100f26e33 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -11,6 +11,8 @@ import { StructureTypeController } from './structure-type/structure-type.control import { StructureTypeService } from './structure-type/structure-type.service'; import { StructureType, StructureTypeSchema } from './structure-type/structure-type.schema'; import { CategoriesModule } from '../categories/categories.module'; +import { StructuresSearchService } from './services/structures-search.service'; +import { SearchModule } from '../search/search.module'; @Module({ imports: [ @@ -23,9 +25,10 @@ import { CategoriesModule } from '../categories/categories.module'; forwardRef(() => UsersModule), CategoriesModule, TempUserModule, + SearchModule, ], controllers: [StructuresController, StructureTypeController], exports: [StructuresService, StructureTypeService], - providers: [StructuresService, StructureTypeService, ApticStructuresService], + providers: [StructuresSearchService, StructuresService, StructureTypeService, ApticStructuresService], }) export class StructuresModule {} diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 584333cf2ae2eb111d14b392e8f5bb1db26664e3..68da5450f95300fde9828f6a2ad2d1905b738753 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,10 +1,13 @@ import { HttpModule } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; +import { CategoriesModule } from '../categories/categories.module'; import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; +import { SearchModule } from '../search/search.module'; import { Structure } from '../structures/schemas/structure.schema'; import { StructuresService } from '../structures/services/structures.service'; +import { StructuresSearchService } from '../structures/services/structures-search.service'; import { TempUser } from '../temp-user/temp-user.schema'; import { TempUserService } from '../temp-user/temp-user.service'; import { User } from './schemas/user.schema'; @@ -16,10 +19,11 @@ describe('UsersController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigurationModule, HttpModule], + imports: [ConfigurationModule, HttpModule, SearchModule], providers: [ UsersService, StructuresService, + StructuresSearchService, MailerService, TempUserService, { diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 46433a721d056558c711c268c0521fe622901523..6f513374851bfd2b405d920bf55e78b56cc5bd5e 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -9,12 +9,14 @@ import { PasswordResetDto } from './dto/reset-password.dto'; import { UsersService } from './users.service'; import { StructuresService } from '../structures/services/structures.service'; import { TempUserService } from '../temp-user/temp-user.service'; +import { ConfigurationService } from '../configuration/configuration.service'; @Controller('users') export class UsersController { constructor( private usersService: UsersService, private structureService: StructuresService, - private tempUserService: TempUserService + private tempUserService: TempUserService, + private configurationService: ConfigurationService ) {} @UseGuards(JwtAuthGuard) @@ -39,6 +41,11 @@ export class UsersController { const user = await this.usersService.create(createUserDto); if (structureId) { this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId); + this.structureService.sendAdminStructureNotification( + null, + this.configurationService.config.templates.adminStructureClaim.ejs, + this.configurationService.config.templates.adminStructureClaim.json + ); } // Remove temp user if exist const tempUser = await this.tempUserService.findOne(createUserDto.email); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 00cb1129c062731347632cc2c3b64359a63e9839..b329eb71fe594731e7b8c0deb6a8825ecdec08de 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -138,43 +138,6 @@ export class UsersService { return user; } - /** - * Send to all admins validation email for structures - * new account. - */ - private async sendAdminStructureValidationMail(): Promise<any> { - const config = this.mailerService.config; - const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureClaim.ejs); - const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureClaim.json); - - const html = await ejs.renderFile(ejsPath, { - config, - }); - const admins = await this.getAdmins(); - admins.forEach((admin) => { - this.mailerService.send(admin.email, jsonConfig.subject, html); - }); - } - - /** - * Send to all admins notification email for new structures - */ - public async sendAdminNewStructureMail(structureName: string, structureId: string): Promise<any> { - const config = this.mailerService.config; - const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureCreate.ejs); - const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureCreate.json); - - const html = await ejs.renderFile(ejsPath, { - config, - name: structureName, - id: structureId, - }); - const admins = await this.getAdmins(); - admins.forEach((admin) => { - this.mailerService.send(admin.email, jsonConfig.subject, html); - }); - } - /** * Send to all admins mail for aptic duplicated data */ @@ -352,7 +315,6 @@ export class UsersService { public async updateStructureLinkedClaim(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure); - this.sendAdminStructureValidationMail(); return stucturesLinked; } diff --git a/template.env b/template.env index 41cb576d15639e668ff90bbdadc2e508f70a9e03..027d8b6a62352ab2d9b95ae9c6d337d9e19d13c6 100644 --- a/template.env +++ b/template.env @@ -20,3 +20,10 @@ GHOST_CONTENT_API_KEY=<Ghost global api key, can be found in integration part of GHOST_ADMIN_API_KEY=<Ghost admin api key, can be found in integration part of ghost UI> GHOST_HOST_AND_PORT=<Ghost host and port, ex:http://localhost:2368> USER_PWD=<test user password, this password will be user by every test users> +ELASTICSEARCH_NODE=<elastic search container node> +ELASTICSEARCH_PATH=<elastic search container path> +ELASTICSEARCH_PORT=<elastic search port> +ELASTICSEARCH_USERNAME=<elastic search username> +ELASTICSEARCH_PASSWORD=<elastic search password> +ELASTIC_SECURITY=<elastic search security boolean (true = secure)> +KIBANA_PORT=<kibana port> \ No newline at end of file