diff --git a/CHANGELOG.md b/CHANGELOG.md index 048c698cd35a989e57e1bc5f3d70f9c53ff5b298..c69c07699c328fc72e212f0ac15544a27aa56290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ 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.5.0](https://forge.grandlyon.com///compare/v1.4.0...v1.5.0) (2021-02-25) + + +### Features + +* add ghost stack ([98e4254](https://forge.grandlyon.com///commit/98e4254ae9cc6c5f91bc7e7ab310b85b52111c33)) +* add name and surname in token ([4a125a1](https://forge.grandlyon.com///commit/4a125a1b4f776baaaa6a7142f4ccc4fe46014f93)) +* add post endpoint and update swagger declaration to allow auth ([d450d44](https://forge.grandlyon.com///commit/d450d44c79acda0262f74e3093a384822f6be733)) +* add startup script for test users ([8d06a19](https://forge.grandlyon.com///commit/8d06a19f4a9c28465a561d720ad5c53db2ee2479)) +* add temp-user handling for structure join ([9560829](https://forge.grandlyon.com///commit/95608291b11759847484efe9b94a1f81a02a3798)) +* add user structure delete ([e592a89](https://forge.grandlyon.com///commit/e592a8979c96a5c03f5280eee21522b52329142b)) +* move structureType to back ([22e0bd2](https://forge.grandlyon.com///commit/22e0bd2eaa6b336a77b4817ccee3d342d1f7d6fd)) +* update config service for template handling + add structure join ([679f239](https://forge.grandlyon.com///commit/679f239edae90f714b60fd7a89e4f881fcc6d5dc)) + + +### Bug Fixes + +* add npm script for db init ([8c819fc](https://forge.grandlyon.com///commit/8c819fc5e250d54e26b9058457534f3f8ddbe1ef)) +* add structure remove from userModel ([fd21790](https://forge.grandlyon.com///commit/fd2179074baef4f435092330ee9169cdae85f803)) +* aptic structure accountVerified ([928bf58](https://forge.grandlyon.com///commit/928bf58b85e49d493e9f462d0fbe55cac21b7b81)) +* aptic structure init + admin structurename for claim validation ([cef5451](https://forge.grandlyon.com///commit/cef545168d9bdd6346767a55fe985459ff81b606)) +* delete structure ([451e004](https://forge.grandlyon.com///commit/451e0041f6c431c57938db34c507dbb48563e2bd)) +* mail config + new url ([874b062](https://forge.grandlyon.com///commit/874b06236254fd6dc28df8fe060f3c0597d01913)) +* MR conflicts ([ac18ce8](https://forge.grandlyon.com///commit/ac18ce82b3456198bb8f0f52638f386d303134ca)) +* MR return + issue on delete owner endpoint ([1fb8047](https://forge.grandlyon.com///commit/1fb80471382d883db23909b4ae58beab05d1cfca)) +* remove unused imports ([f2db14e](https://forge.grandlyon.com///commit/f2db14eb0bb98682dbb3c37610e88f800480f9d5)) +* remove useless swagger auth ([bf89fbd](https://forge.grandlyon.com///commit/bf89fbd62873517fdb990a7a2b9126e296b0ddcd)) +* user cration bug + structure find refacto ([25480d5](https://forge.grandlyon.com///commit/25480d585119ca6bef98dc4567cd63c8b273e980)) +* user model structureid from string to objectid ([35fa953](https://forge.grandlyon.com///commit/35fa953761b6800426155d27595efaa26df0f47f)) +* **form:** fix model ([f0d1df6](https://forge.grandlyon.com///commit/f0d1df6092c099a100256425c1ab4b640b48aaf6)) +* **form:** fix structure ([af1b4fa](https://forge.grandlyon.com///commit/af1b4fac5fa2c370e53917dfe3271c3c60a55266)) + ## [1.4.0](https://forge.grandlyon.com///compare/v1.3.0...v1.4.0) (2021-02-01) diff --git a/db/init-db.sh b/db/init-db.sh deleted file mode 100644 index eb93fd77a432ad1f338ab29c6a3941c916ba8040..0000000000000000000000000000000000000000 --- a/db/init-db.sh +++ /dev/null @@ -1,5 +0,0 @@ -mongo admin -u root -p MONGO_ROOT_PASSWORD -use ram -db.structures.insertOne({"id":1,"numero":"26-190","dateDeCreation":"Mon Nov 16 2020 16:37:00 GMT+0100 (heure normale d’Europe centrale)","derniereModification":"Mon Nov 16 2020 16:38:00 GMT+0100 (heure normale d’Europe centrale)","nomDeLusager":"-","votreStructureEstElle":"Un établissement principal (siège social)","nomDeVotreStructure":"Maison de l'Emploi de Feyzin","description":"- Permanence de médiation numérique (sur rdv) les lundis; mardi, mercredi apres midi 13h30 - 17h30 et les vendredi matins 9h-12h - mise à disposition d'ordinateurs aux horaires d'ouverture de la structure lundi, mercredi, jeudi 9h-12h; 13h30-17h30 ; mardi 10h-12h 13h30- 17h30 et vendredi matin 9h-12H. - mise à disposition (sur rdv)de matériels pour suivre des formations / informations collectives à distance et participer à un entretien en visio - cours de code et entrainement en ligne sur rdv","activitesMaintenuesDansLeCadreDuConfinement":"Toutes les activités sont maintenues durant le confinement. Toutes nécessitent cependant de prendre rdv préalablement","n":"18","voie":"Rue de la Mairie","telephone":"04 72 21 46 66","courriel":"m.annequin@ville-feyzin.fr","siteWeb":"ville de Feyzin","facebook":"","twitter":"","instagram":"","civilite":"Madame","nom":"Annequin","prenom":"Muriel","fonction":"Directeur","accessibilitePersonnesAMobiliteReduitePmr":"True","modalitesDacces":["Accès libre","Sur RDV"],"pourLesRdv,MerciDePreciserSilEstNecessaireDapporterDesPiecesJustificativesOuDuMateriel.":"Si la personne n'est pas inscrite à la maison de l'emploi il lui sera demandé de s'inscrire.","labelsEtQualifications":[""],"publicsAcceptes":["Tout public"],"fermeturesExceptionnelles":"Jours fériés","jaccompagneLesUsagersDansLeursDemarchesEnLigne":"True","accompagnementDesDemarches":["Pôle Emploi","Accompagnant CAF"],"autresAccompagnements":"","lesCompetencesDeBase":[""],"accesAuxDroits":["176"],"insertionSocialeEtProfessionnelle":[""],"aideALaParentalite":[""],"cultureEtSecuriteNumerique":[""],"wifiEnAccesLibre":"True","ordinateurs":"True","nombre":"5","tablettes":"","bornesNumeriques":"","imprimantes":"True","precisionsSiNecessaire":"","statutJuridique":"","appartenezVousAUnReseauDeMediation":"","precisezLequel":"","idDeLitemStructureDansDirectus":"141","statutDeLitemStructureDansDirectus":"","idDeLitemOffreDansDirectus":"46","statut":"Versement des données offre","typeDeStructure":["Mairie"],"commune":"Feyzin","hours":{"monday":{"open":true,"time":[{"openning":900,"closing":1200},{"openning":1330,"closing":1730}]},"tuesday":{"open":true,"time":[{"openning":1000,"closing":1200},{"openning":1330,"closing":1730}]},"wednesday":{"open":true,"time":[{"openning":900,"closing":1200},{"openning":1330,"closing":1730}]},"thursday":{"open":true,"time":[{"openning":900,"closing":1200},{"openning":1330,"closing":1730}]},"friday":{"open":true,"time":[{"openning":900,"closing":1200},{"openning":null,"closing":null}]},"saturday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"sunday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]}}}) -db.structures.insertOne({"id":2,"numero":"26-182","dateDeCreation":"Mon Nov 16 2020 15:15:00 GMT+0100 (heure normale d’Europe centrale)","derniereModification":"Tue Nov 17 2020 09:10:00 GMT+0100 (heure normale d’Europe centrale)","nomDeLusager":"-","votreStructureEstElle":"Un établissement principal (siège social)","nomDeVotreStructure":"Centre social Quartier Vitalité","description":"Le centre social s'inscrit dans une démarche d'animation de la vie sociale locale afin de permettre aux habitants d'exprimer, de concevoir, de réaliser leurs projets. C'est un lieu de proximité à vocation familiale et intergénérationnelle qui propose des activités sociales, éducatives et culturelles pour répondre aux besoins de chacun.","activitesMaintenuesDansLeCadreDuConfinement":"Aide aux démarches administratives et médiation numérique, sur rendez vous uniquement","n":"7","voie":"Rue Saint Polycarpe","telephone":"04.78.39.36.36","courriel":"aurebertherat@gmail.com","siteWeb":"www.centresocialquartiervitalite.i-citoyen.com","facebook":"","twitter":"","instagram":"","civilite":"Madame","nom":"BERTHERAT","prenom":"AURELIE","fonction":"Autres","accessibilitePersonnesAMobiliteReduitePmr":"True","modalitesDacces":["Téléphone / Visio","Sur RDV"],"pourLesRdv,MerciDePreciserSilEstNecessaireDapporterDesPiecesJustificativesOuDuMateriel.":"","labelsEtQualifications":[""],"publicsAcceptes":["Jeunes (16-25 ans)","Adultes","Séniors (+ de 65 ans)","Tout public"],"fermeturesExceptionnelles":"vacances de Noel + tout le mois d'Aout","jaccompagneLesUsagersDansLeursDemarchesEnLigne":"True","accompagnementDesDemarches":["Pôle Emploi","CPAM","Impôts","Logement","CARSAT","Accompagnant CAF"],"autresAccompagnements":"","lesCompetencesDeBase":["259","261","249","222","212","186","183","260"],"accesAuxDroits":["175","174","173","172","171","167","165","176"],"insertionSocialeEtProfessionnelle":["193","192","191","262","194"],"aideALaParentalite":["238","178","166","257"],"cultureEtSecuriteNumerique":["232","225","221","218","208","195","264"],"wifiEnAccesLibre":"","ordinateurs":"True","nombre":"3","tablettes":"","bornesNumeriques":"","imprimantes":"True","precisionsSiNecessaire":"Impression de documents uniquement en lien avec le rendez vous et l'accompagnement administratif de la personne","statutJuridique":"","appartenezVousAUnReseauDeMediation":"","precisezLequel":"","idDeLitemStructureDansDirectus":"140","statutDeLitemStructureDansDirectus":"","idDeLitemOffreDansDirectus":"","statut":"Erreur lors du versement des données offre","typeDeStructure":["Centre socio-culturel"],"commune":"Lyon 1","hours":{"monday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"tuesday":{"open":true,"time":[{"openning":1330,"closing":1700},{"openning":null,"closing":null}]},"wednesday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"thursday":{"open":true,"time":[{"openning":930,"closing":1200},{"openning":null,"closing":null}]},"friday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"saturday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"sunday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]}}}) -db.structures.insertOne({"id":3,"numero":"26-181","dateDeCreation":"Mon Nov 16 2020 12:49:00 GMT+0100 (heure normale d’Europe centrale)","derniereModification":"Tue Nov 17 2020 09:12:00 GMT+0100 (heure normale d’Europe centrale)","nomDeLusager":"-","votreStructureEstElle":"Un établissement principal (siège social)","nomDeVotreStructure":"Centre socioculturel la Carnière","description":"Le centre socio-culturel la Carnière est une association développe dans le cadre de son projet des activités, des services et un soutien aux projets et initiatives ouvert à tous les habitants de la commune. En phase avec les enjeux du territoire, nous œuvrons plus particulièrement dans les domaines de l'accompagnement social et l'accès aux droits, le handicap, la fracture numérique, la parentalité et enfin auprès des personnes âgées.","activitesMaintenuesDansLeCadreDuConfinement":"- WEBTV pour garder le contact et proposer des activités à faire à la maison en direct en replay. - Accompagnement et médiation numérique (démarche en ligne, accès au droit...) - Handicap et numérique. - Entraide numérique sur rdv individuel (sortir d'un blocage, découvrir skype, zoom...) - Ateliers linguistiques via l'outil numérique. - Accompagnement scolaire à distance. - Conférence parentalité en visio.","n":"4","voie":"Montée de la Carnière","telephone":"0478206197","courriel":"contact@centresocial-lacarniere.fr ou numerique@centresocial-lacarniere.fr","siteWeb":"www.centresocial-lacarniere.fr","facebook":"","twitter":"","instagram":"","civilite":"Monsieur","nom":"Chanteperdrix Cyril ou Haouchet Karim","prenom":"","fonction":"Directeur","accessibilitePersonnesAMobiliteReduitePmr":"True","modalitesDacces":["Téléphone / Visio","Sur RDV"],"pourLesRdv,MerciDePreciserSilEstNecessaireDapporterDesPiecesJustificativesOuDuMateriel.":"ordinateur portable habituel.","labelsEtQualifications":["Pass numérique"],"publicsAcceptes":["Tout public"],"fermeturesExceptionnelles":"","jaccompagneLesUsagersDansLeursDemarchesEnLigne":"True","accompagnementDesDemarches":["Pôle Emploi","CPAM","Impôts","Logement","CARSAT","Accompagnant CAF"],"autresAccompagnements":"","lesCompetencesDeBase":["259","261","249","222","212","186","183","260"],"accesAuxDroits":["175","174","173","172","171","167","165","176"],"insertionSocialeEtProfessionnelle":[""],"aideALaParentalite":["178","166","257"],"cultureEtSecuriteNumerique":["255","232","225","221","218","208","195","164","163","162","264"],"wifiEnAccesLibre":"True","ordinateurs":"","nombre":"","tablettes":"","bornesNumeriques":"","imprimantes":"","precisionsSiNecessaire":"équipements informatiques fournis pour les formations","statutJuridique":"","appartenezVousAUnReseauDeMediation":"","precisezLequel":"","idDeLitemStructureDansDirectus":"143","statutDeLitemStructureDansDirectus":"","idDeLitemOffreDansDirectus":"","statut":"Erreur lors du versement des données offre","typeDeStructure":["Centre socio-culturel"],"commune":"Saint-Priest","hours":{"monday":{"open":true,"time":[{"openning":900,"closing":1800},{"openning":null,"closing":null}]},"tuesday":{"open":true,"time":[{"openning":1400,"closing":1730},{"openning":null,"closing":null}]},"wednesday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"thursday":{"open":true,"time":[{"openning":900,"closing":1800},{"openning":null,"closing":null}]},"friday":{"open":true,"time":[{"openning":900,"closing":1730},{"openning":null,"closing":null}]},"saturday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]},"sunday":{"open":false,"time":[{"openning":null,"closing":null},{"openning":null,"closing":null}]}}}) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6c7ead9599e89777d27cae37032fb9b2a8b227fc..e95e22ec421be34eadca84b2076cc2b029fd0cb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: image: registry.forge.grandlyon.com/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server:${TAG} ports: - ${SERVICE_API_BIND_PORT}:3000 + extra_hosts: + - 'sen.grandlyon.com:10.128.16.229' environment: MONGO_NON_ROOT_USERNAME: ${MONGO_NON_ROOT_USERNAME} MONGO_NON_ROOT_PASSWORD: ${MONGO_NON_ROOT_PASSWORD} diff --git a/package-lock.json b/package-lock.json index 9548a30e3c319f89eff48e94dcceb9edc9792677..dc0afa3bbfbaacb0be4f90c6e7336c6851f46bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ram_server", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3bafee71b4d5b8fe6f16d032967a56adaf5d6dbb..9a2df72b30dfe40e182752e11988b88bbd3da23c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ram_server", "private": true, - "version": "1.4.0", + "version": "1.5.0", "description": "Nest TypeScript starter repository", "license": "MIT", "scripts": { @@ -14,6 +14,7 @@ "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "release": "standard-version", + "init-db": "node ./scripts/init-db.js", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", diff --git a/scripts/data/users.js b/scripts/data/users.js new file mode 100644 index 0000000000000000000000000000000000000000..f678dc4d10096167762ac8c30bd43624922de1ff --- /dev/null +++ b/scripts/data/users.js @@ -0,0 +1,55 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const mongoose = require('mongoose'); + +module.exports = { + data: [ + { + pendingStructuresLink: [], + structuresLink: [ + mongoose.Types.ObjectId('60059cf7dfb2ac4b00733db0'), + mongoose.Types.ObjectId('6001a48e16b08100062e4180'), + ], + phone: '06 06 06 06 06', + newEmail: null, + changeEmailToken: null, + role: 1, + resetPasswordToken: null, + surname: 'ADMIN', + name: 'Admin', + validationToken: null, + emailVerified: true, + email: 'admin@admin.com', + structureOutdatedMailSent: [], + }, + { + structureOutdatedMailSent: [], + pendingStructuresLink: [], + structuresLink: [mongoose.Types.ObjectId('6001a48e16b08100062e4180')], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'paula.dubois@mii.com', + name: 'Paula', + surname: 'DUBOIS', + phone: '06 07 08 09 10', + }, + { + structureOutdatedMailSent: [], + pendingStructuresLink: [], + structuresLink: [mongoose.Types.ObjectId('6001a48e16b08100062e4180')], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'jp@test.com', + name: 'Jean-Paul', + surname: 'DESCHAMPS', + phone: '06 11 11 11 11', + }, + ], +}; diff --git a/scripts/init-db.js b/scripts/init-db.js new file mode 100644 index 0000000000000000000000000000000000000000..d65251eb5ed7bb7b8674f964aeef13466a8bced6 --- /dev/null +++ b/scripts/init-db.js @@ -0,0 +1,71 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const mongoose = require('mongoose'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const userData = require('./data/users'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const bcrypt = require('bcrypt'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +// const app = require(path.resolve(__dirname, './server')); +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); + +/* connect to the database */ +const param = + 'mongodb://' + + process.env.MONGO_NON_ROOT_USERNAME + + ':' + + process.env.MONGO_NON_ROOT_PASSWORD + + '@' + + process.env.MONGO_DB_HOST_AND_PORT + + '/ram'; +mongoose.connect(param, { useNewUrlParser: true, useUnifiedTopology: true }).catch((error) => console.log(error)); +// Make Mongoose use `findOneAndUpdate()`. Note that this option is `true` +// by default, you need to set it to false. +mongoose.set('useFindAndModify', false); + +function hashPassword() { + return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT); +} + +// define Schema +var usersSchema = mongoose.Schema({ + name: String, + surname: String, + email: String, + emailVerified: Boolean, + validationToken: String, + resetPasswordToken: String, + role: Number, + changeEmailToken: String, + newEmail: String, + structuresLink: [], + structureOutdatedMailSent: [], + pendingStructuresLink: [], + password: String, + phone: String, +}); + +// compile schema to model +var User = mongoose.model('Users', usersSchema); + +/* drop users collections */ +mongoose.connection.dropCollection('users', function (err) { + /* show messages */ + if (err) { + if (err.code === 26) console.log('-- Users collection does not exists --'); + else throw err; + } else console.log('-- Users collection dropped --'); + + // Init passsword + console.log('-- Users password encryption based on .env --'); + userData.data.forEach((user) => { + user.password = hashPassword(); + }); + // save model to database + User.create(userData.data, function (error, user) { + if (error) return console.error(error); + console.log('-- Users collection initialized --'); + process.exit(0); + }); +}); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index fe16c656f87b81aa1387fddd78904e413ab715da..a3cb5fea6e5bab0a79fea575f49d689658e80238 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -16,8 +16,14 @@ export class AdminController { @Roles('admin') @Get('pendingStructures') @ApiOperation({ description: 'Get pending structre for validation' }) - public getPendingAttachments(): Promise<PendingStructureDto[]> { - return this.usersService.getPendingStructures(); + public async getPendingAttachments(): Promise<PendingStructureDto[]> { + const pendingStructure = await this.usersService.getPendingStructures(); + return await Promise.all( + pendingStructure.map(async (structure) => { + structure.structureName = (await this.structuresService.findOne(structure.structureId)).structureName; + return structure; + }) + ); } @UseGuards(JwtAuthGuard, RolesGuard) diff --git a/src/admin/dto/pending-structure.dto.ts b/src/admin/dto/pending-structure.dto.ts index 67db3710579161c25baefdce81f0462a7e73cdb6..e07fd91f860cc5468be1e2449ce05897a26b1465 100644 --- a/src/admin/dto/pending-structure.dto.ts +++ b/src/admin/dto/pending-structure.dto.ts @@ -11,4 +11,9 @@ export class PendingStructureDto { @IsString() @ApiProperty({ type: String }) readonly structureId: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ type: String }) + structureName: string; } diff --git a/src/app.module.ts b/src/app.module.ts index e0aa57e2278e56f027bcfe0d0db969e5936708c8..467fe404c31f6fdeadc948aec96bdf136aa676f3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { MailerModule } from './mailer/mailer.module'; import { TclModule } from './tcl/tcl.module'; import { AdminModule } from './admin/admin.module'; import { PostsModule } from './posts/posts.module'; +import { TempUserModule } from './temp-user/temp-user.module'; @Module({ imports: [ ConfigurationModule, @@ -26,6 +27,7 @@ import { PostsModule } from './posts/posts.module'; TclModule, AdminModule, PostsModule, + TempUserModule, ], controllers: [AppController], }) diff --git a/src/configuration/config.dev.ts b/src/configuration/config.dev.ts index e9763084fa20e80748ce3f82c33fe8f3517bc528..90917c6903316ef855ee1e2d21e7b93dcb7aa76a 100644 --- a/src/configuration/config.dev.ts +++ b/src/configuration/config.dev.ts @@ -1,37 +1,10 @@ export const configDev = { url: process.env.MAIL_URL, token: process.env.MAIL_TOKEN, - host: 'ram-dev.grandlyon.com', + host: 'resin-dev.grandlyon.com', protocol: 'https', port: '443', from: 'inclusionnumerique@grandlyon.com', from_name: 'Réseau des acteurs de la médiation numérique', replyTo: 'inclusionnumerique@grandlyon.com', - templates: { - directory: './src/mailer/mail-templates', - verify: { - ejs: 'verify.ejs', - json: 'verify.json', - }, - changeEmail: { - ejs: 'changeEmail.ejs', - json: 'changeEmail.json', - }, - resetPassword: { - ejs: 'resetPassword.ejs', - json: 'resetPassword.json', - }, - adminStructureClaim: { - ejs: 'adminStructureClaim.ejs', - json: 'adminStructureClaim.json', - }, - structureClaimValidation: { - ejs: 'structureClaimValidation.ejs', - json: 'structureClaimValidation.json', - }, - structureOutdatedInfo: { - ejs: 'structureOutdatedInfo.ejs', - json: 'structureOutdatedInfo.json', - }, - }, }; diff --git a/src/configuration/config.prod.ts b/src/configuration/config.prod.ts index 7c1b236c9b2d50c67e4769354b0d3dcded212fa8..d9333dd6499c2498a7853abf5ad97b8e1a2c849c 100644 --- a/src/configuration/config.prod.ts +++ b/src/configuration/config.prod.ts @@ -1,37 +1,10 @@ export const configProd = { url: process.env.MAIL_URL, token: process.env.MAIL_TOKEN, - host: 'ram.grandlyon.com', + host: 'resin.grandlyon.com', protocol: 'https', port: '443', from: 'inclusionnumerique@grandlyon.com', from_name: 'Réseau des acteurs de la médiation numérique', replyTo: 'inclusionnumerique@grandlyon.com', - templates: { - directory: './src/mailer/mail-templates', - verify: { - ejs: 'verify.ejs', - json: 'verify.json', - }, - changeEmail: { - ejs: 'changeEmail.ejs', - json: 'changeEmail.json', - }, - resetPassword: { - ejs: 'resetPassword.ejs', - json: 'resetPassword.json', - }, - adminStructureClaim: { - ejs: 'adminStructureClaim.ejs', - json: 'adminStructureClaim.json', - }, - structureClaimValidation: { - ejs: 'structureClaimValidation.ejs', - json: 'structureClaimValidation.json', - }, - structureOutdatedInfo: { - ejs: 'structureOutdatedInfo.ejs', - json: 'structureOutdatedInfo.json', - }, - }, }; diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 7bb9d6f5a8b02c9fc654caa84f5cb74a223c99e9..e9d940409b5b8b927e138bb8b33a9e2ff6935743 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -37,5 +37,13 @@ export const config = { ejs: 'apticStructureDuplication.ejs', json: 'apticStructureDuplication.json', }, + tempUserRegistration: { + ejs: 'tempUserRegistration.ejs', + json: 'tempUserRegistration.json', + }, + structureJoinRequest: { + ejs: 'structureJoinRequest.ejs', + json: 'structureJoinRequest.json', + }, }, }; diff --git a/src/configuration/configuration.service.ts b/src/configuration/configuration.service.ts index 13b4f5e4d827e55bc85ba4cacb6dc54ada5c887c..ca6f9e6c44a78e2259c3b3e70c8a5168d3d30d95 100644 --- a/src/configuration/configuration.service.ts +++ b/src/configuration/configuration.service.ts @@ -10,9 +10,11 @@ export class ConfigurationService { // Initializing conf with values from var env if (process.env.NODE_ENV && process.env.NODE_ENV === 'production') { this._config = configProd; + this._config.templates = config.templates; // Add mail templates Logger.log('App started with production conf', 'ConfigurationService'); } else if (process.env.NODE_ENV && process.env.NODE_ENV === 'dev') { this._config = configDev; + this._config.templates = config.templates; // Add mail templates Logger.log('App started with dev conf', 'ConfigurationService'); } else { this._config = config; diff --git a/src/mailer/mail-templates/structureJoinRequest.ejs b/src/mailer/mail-templates/structureJoinRequest.ejs new file mode 100644 index 0000000000000000000000000000000000000000..550665e04bb48c9f796a21b97fef4dfc11407107 --- /dev/null +++ b/src/mailer/mail-templates/structureJoinRequest.ejs @@ -0,0 +1,21 @@ +Bonjour<br /> +<br /> +Vous recevez ce message car <strong><%= surname %></strong> <strong><%= name %></strong> demande a 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 + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join?id=<%= id %>&userId=<%= userId %>&status=true" + >cliquant ici</a +> +ou refuser la demande +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join?id=<%= id %>&userId=<%= userId %>&status=false" + >cliquant ici</a +>. +<br /> +Cordialement, +<br /> +L'équipe RES'in +<br /> +<br /> +Ce mail est un mail automatique. Merci de ne pas y répondre. diff --git a/src/mailer/mail-templates/structureJoinRequest.json b/src/mailer/mail-templates/structureJoinRequest.json new file mode 100644 index 0000000000000000000000000000000000000000..8b756e80dda646b74494d044a1c027c48294ac34 --- /dev/null +++ b/src/mailer/mail-templates/structureJoinRequest.json @@ -0,0 +1,3 @@ +{ + "subject": "Un acteur demande a rejoindre votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/mailer/mail-templates/tempUserRegistration.ejs b/src/mailer/mail-templates/tempUserRegistration.ejs new file mode 100644 index 0000000000000000000000000000000000000000..8422b7dbefde902844d6970d16a0b19282321a92 --- /dev/null +++ b/src/mailer/mail-templates/tempUserRegistration.ejs @@ -0,0 +1,15 @@ +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 +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 %>" + >cliquant ici</a +>. +<br /> +Cordialement, +<br /> +L'équipe RES'in +<br /> +<br /> +Ce mail est un mail automatique. Merci de ne pas y répondre. diff --git a/src/mailer/mail-templates/tempUserRegistration.json b/src/mailer/mail-templates/tempUserRegistration.json new file mode 100644 index 0000000000000000000000000000000000000000..b344e7953f3643b216eec56a385e464f4d6a0de6 --- /dev/null +++ b/src/mailer/mail-templates/tempUserRegistration.json @@ -0,0 +1,3 @@ +{ + "subject": "Un compte a été créé pour vous sur le Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index e5c9478a2cde8a29629759fab5fbf5513c07acc0..69d709984424f4be5bc48a1c53b9ca22c085e6f9 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, HttpService, Query } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { ApiQuery } from '@nestjs/swagger'; import { Post } from './schemas/post.schema'; @Controller('posts') diff --git a/src/structures/dto/structure.dto.ts b/src/structures/dto/structure.dto.ts index 01c7a962917949ce57fcaed7a0f3a424a3a00feb..b4e417214280f76147dde7e07c55efc70d2a0100 100644 --- a/src/structures/dto/structure.dto.ts +++ b/src/structures/dto/structure.dto.ts @@ -7,6 +7,7 @@ export class structureDto { numero: string; createdAt: string; updatedAt: string; + deletedAt: Date; @IsNotEmpty() structureName: string; @@ -46,8 +47,6 @@ export class structureDto { @ArrayNotEmpty() publics: string[]; @IsNotEmpty() - freeWifi: boolean; - @IsNotEmpty() freeWorkShop: boolean; @IsNotEmpty() nbComputers: number; diff --git a/src/structures/schemas/address.schema.ts b/src/structures/schemas/address.schema.ts index f0d34d8f46936c871bb1299f88f9445d80c8dcd9..8fa4e35c5c469dcfd2a36d88b78faf7d1575a93f 100644 --- a/src/structures/schemas/address.schema.ts +++ b/src/structures/schemas/address.schema.ts @@ -4,7 +4,6 @@ import { IsNotEmpty } from 'class-validator'; export type AddressDocument = Address & Document; export class Address { - @IsNotEmpty() numero: string; @IsNotEmpty() diff --git a/src/structures/schemas/structure.schema.ts b/src/structures/schemas/structure.schema.ts index b782161cbffe5c89fae607f26b8ed3756b77fc59..fdd977bfc67d6f6ab411d60b1ba36f109a4a5573 100644 --- a/src/structures/schemas/structure.schema.ts +++ b/src/structures/schemas/structure.schema.ts @@ -1,5 +1,5 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document, Types } from 'mongoose'; +import { Document } from 'mongoose'; import { Address } from './address.schema'; import { Week } from './week.schema'; @@ -93,9 +93,6 @@ export class Structure { @Prop() equipmentsAndServices: string[]; - @Prop() - freeWifi: boolean; - @Prop() freeWorkShop: boolean; diff --git a/src/structures/services/aptic-structures.service.ts b/src/structures/services/aptic-structures.service.ts index f2bd9fdc20670bad14a289eb1a29ddaff6f7cc26..fff5c3326547acc5f68bebbc93d0e81ef050b198 100644 --- a/src/structures/services/aptic-structures.service.ts +++ b/src/structures/services/aptic-structures.service.ts @@ -49,11 +49,25 @@ export class ApticStructuresService { if (!exist) { Logger.log(`Create structure : ${structure.presence_name}`, 'ApticStructuresService - createApticStructures'); const createdStructure = new this.structureModel(); - createdStructure.coord = [structure.gps_lng, structure.gps_lat]; + // Known fields createdStructure.structureName = structure.presence_name; createdStructure.contactPhone = structure.presence_phone; + // Unkown fields (but mandatory) + createdStructure.contactMail = 'unknown@unknown.com'; createdStructure.labelsQualifications = ['passNumerique']; + createdStructure.structureType = 'autre'; + createdStructure.pmrAccess = false; + createdStructure.accessModality = ['accesLibre']; + createdStructure.publics = ['toutPublic']; + createdStructure.accountVerified = true; + createdStructure.freeWorkShop = false; + createdStructure.nbComputers = null; + createdStructure.nbPrinters = null; + createdStructure.nbScanners = null; + createdStructure.nbTablets = null; + createdStructure.nbNumericTerminal = null; // Address + createdStructure.coord = [structure.gps_lng, structure.gps_lat]; createdStructure.address = this.formatAddress(structure); createdStructure.save(); // Send admin weird structure mail diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index f574d12fdf0a4c5369b33d0a379a79c2d68bd682..045ac5a8a821215393fa5a0b22b998f84ceee027 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -11,6 +11,9 @@ import { User } from '../../users/schemas/user.schema'; import { MailerService } from '../../mailer/mailer.service'; import { Cron, CronExpression } from '@nestjs/schedule'; import { DateTime } from 'luxon'; +import { IUser } from '../../users/interfaces/user.interface'; +import * as _ from 'lodash'; +import { OwnerDto } from '../../users/dto/owner.dto'; @Injectable() export class StructuresService { @@ -21,12 +24,12 @@ export class StructuresService { @InjectModel(Structure.name) private structureModel: Model<StructureDocument> ) {} - public async create(idUser: string, structureDto: structureDto): Promise<Structure> { + public async create(idUser: string, structure: structureDto): Promise<Structure> { const user = await this.userService.findOne(idUser); if (!user) { throw new HttpException('Invalid profile', HttpStatus.NOT_FOUND); } - const createdStructure = new this.structureModel(structureDto); + const createdStructure = new this.structureModel(structure); createdStructure._id = Types.ObjectId(); createdStructure.save(); user.structuresLink.push(createdStructure._id); @@ -79,31 +82,23 @@ export class StructuresService { await Promise.all( structures.map((structure: StructureDocument) => { // If structre has no address, add it - if (!structure.address) { - return this.getStructurePosition(structure).then((postition) => { + 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(); }); } - if (structure.coord.length <= 0) { - return new Promise((resolve) => { - this.getStructurePosition(structure).then((postition: StructureDocument) => { - this.structureModel - .findByIdAndUpdate(Types.ObjectId(postition._id), { coord: postition.coord }) - .exec() - .then(() => { - resolve(''); - }); - }); - }); - } }) ); return this.structureModel.find({ deletedAt: { $exists: false }, accountVerified: true }).exec(); } public async update(idStructure: string, structure: structureDto): Promise<Structure> { + const oldStructure = await this.findOne(idStructure); + if (!_.isEqual(oldStructure.address, structure.address)) { + await this.getStructurePosition(structure).then(); + } const result = await this.structureModel.findByIdAndUpdate(Types.ObjectId(idStructure), structure).exec(); if (!result) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); @@ -113,18 +108,26 @@ export class StructuresService { return this.findOne(idStructure); } - public async findOne(idParam: string): Promise<Structure> { + public async findOne(idParam: string): Promise<StructureDocument> { return await this.structureModel.findById(Types.ObjectId(idParam)).exec(); } /** * Get structures positions and add marker corresponding to those positons on the map */ private getStructurePosition(structure: Structure): Promise<Structure> { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.getCoord(structure.address.numero, structure.address.street, structure.address.commune).subscribe( (res) => { const address = res.data.features[0]; - structure.coord = address.geometry.coordinates; + if (address && address.geometry) { + structure.coord = address.geometry.coordinates; + } else { + Logger.error( + `No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`, + 'StructureService' + ); + structure.coord = []; + } resolve(structure); }, (err) => { @@ -226,17 +229,20 @@ export class StructuresService { if (!structure) { throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); } + structure.structureType = null; structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString(); this.anonymizeStructure(structure).save(); + // Remove structure from userModel + this.userService.removeStructureIdFromUsers(structure._id); return structure; } private anonymizeStructure(structure: StructureDocument): StructureDocument { structure.contactPhone = ''; structure.contactMail = ''; - structure.facebook = ''; - structure.twitter = ''; - structure.instagram = ''; + structure.facebook = null; + structure.twitter = null; + structure.instagram = null; structure.website = ''; return structure; } @@ -281,7 +287,7 @@ export class StructuresService { } /** - * Generate activation token and send it to user by email, in order to validate + * Send an email to prevent outdated * a new account. * @param user User */ @@ -298,9 +304,36 @@ export class StructuresService { this.mailerService.send(userEmail, jsonConfig.subject, html); } + /** + * Send an email to structure owner's in order to accept or decline a join request + * @param user User + */ + public async sendStructureJoinRequest(user: IUser, structure: StructureDocument): Promise<void> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureJoinRequest.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureJoinRequest.json); + + const html = await ejs.renderFile(ejsPath, { + config, + structureName: structure.structureName, + name: user.name, + surname: user.surname, + id: structure._id, + userId: user._id, + }); + const owners = await this.getOwners(structure._id); + owners.forEach((owner) => { + this.mailerService.send(owner.email, jsonConfig.subject, html); + }); + } + + private async getOwners(structureId: string): Promise<IUser[]> { + // Get owners of outdated structures + return this.userService.getStructureOwners(structureId); + } + public async updateAccountVerified(idStructure: string, emailUser: string): Promise<Structure> { - const user = await this.userService.findOne(emailUser); - const structureLinked = await this.findOne(user.structuresLink[0].toHexString()); + const structureLinked = await this.findOne(idStructure); const structure = new this.structureModel(structureLinked); if (!structure) { throw new HttpException('Invalid structure', HttpStatus.NOT_FOUND); @@ -309,4 +342,16 @@ export class StructuresService { structure.save(); return structure; } + + public async findWithOwners( + idStructure: string, + emailUser: string + ): Promise<{ structure: Structure; owners: OwnerDto[] }> { + const structure = await this.findOne(idStructure); + if (!structure) { + throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); + } + const owners = await this.userService.getStructureOwnersMails(idStructure, emailUser); + return { structure: structure, owners: owners }; + } } diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index c3fb6ef641f799d45b46b427bc9fe26118f73313..b4aeea9bc83c7d3a6198af2d44909e4300b60f89 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -1,10 +1,23 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; import { ApiParam } from '@nestjs/swagger'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +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'; import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard'; -import { RolesGuard } from '../users/guards/roles.guard'; import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/users.service'; import { CreateStructureDto } from './dto/create-structure.dto'; @@ -15,7 +28,11 @@ import { StructuresService } from './services/structures.service'; @Controller('structures') export class StructuresController { - constructor(private readonly structureService: StructuresService, private readonly userService: UsersService) {} + constructor( + private readonly structureService: StructuresService, + private readonly userService: UsersService, + private readonly tempUserService: TempUserService + ) {} @Post() public async create(@Body() createStructureDto: CreateStructureDto): Promise<Structure> { @@ -54,7 +71,7 @@ export class StructuresController { @Post(':id/claim') public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> { - return this.userService.updateStructureLinked(user.email, idStructure); + return this.userService.updateStructureLinkedClaim(user.email, idStructure); } @Get('count') @@ -88,11 +105,116 @@ export class StructuresController { return this.structureService.findOne(id); } + @Post(':id/withOwners') + public async findWithOwners(@Param('id') id: string, @Body() data: { emailUser: string }) { + return this.structureService.findWithOwners(id, data.emailUser); + } + @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) @Roles('admin') @ApiParam({ name: 'id', type: String, required: true }) public async delete(@Param('id') id: string) { return this.structureService.deleteOne(id); } + + @Post(':id/addOwner') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @Roles('admin') + @ApiParam({ name: 'id', type: String, required: true }) + public async addOwner(@Param('id') id: string, @Body() user: CreateTempUserDto): Promise<any> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + user.pendingStructuresLink = [Types.ObjectId(id)]; + // If user already exist, use created account + if (await this.userService.verifyUserExist(user.email)) { + return this.userService.updateStructureLinked(user.email, id); + } + // If temp user exist, update it + if (await this.tempUserService.findOne(user.email)) { + return this.tempUserService.updateStructureLinked(user); + } + // If not, create + return this.tempUserService.create(user, structure.structureName); + } + + @Post(':id/join') + @ApiParam({ name: 'id', type: String, required: true }) + public async join(@Param('id') id: string, @Body() user: User): Promise<void> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user and add pending structure + const userFromDb = await this.userService.findOne(user.email); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + // If user has not already request it, send owner's validation email + if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) { + userFromDb.pendingStructuresLink.push(Types.ObjectId(id)); + userFromDb.save(); + // Send structure owner's an email + this.structureService.sendStructureJoinRequest(userFromDb, structure); + } + } + + @Post(':id/join/:userId/:status') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @ApiParam({ name: 'id', type: String, required: true }) + @ApiParam({ name: 'userId', type: String, required: true }) + @ApiParam({ name: 'status', type: String, required: true }) + public async joinValidation( + @Param('id') id: string, + @Param('status') status: string, + @Param('userId') userId: string + ): Promise<any> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + + // Get user and add pending structure + const userFromDb = await this.userService.findById(userId); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + + if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) { + throw new HttpException('User not linked to structure', HttpStatus.NOT_FOUND); + } + + if (status === 'true') { + // Accept + await this.userService.updateStructureLinked(userFromDb.email, id); + await this.userService.removeFromPendingStructureLinked(userFromDb.email, id); + } else { + // Refuse + this.userService.removeFromPendingStructureLinked(userFromDb.email, id); + } + } + + @Delete(':id/owner/:userId') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @Roles('admin') + @ApiParam({ name: 'id', type: String, required: true }) + @ApiParam({ name: 'userId', type: String, required: true }) + public async removeOwner(@Param('id') id: string, @Param('userId') userId: string): Promise<void> { + // Get structure + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user + const userFromDb = await this.userService.findById(userId); + if (!userFromDb || !userFromDb.structuresLink.includes(Types.ObjectId(id))) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + this.userService.removeFromStructureLinked(userFromDb.email, id); + } } diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 6494ea3a5eed4386ed251679c6e5b45a66e88804..51ec4d11ca7c6d9640944f69b156cfaac87f1aa3 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -1,5 +1,6 @@ -import { HttpModule, Module } from '@nestjs/common'; +import { forwardRef, HttpModule, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { TempUserModule } from '../temp-user/temp-user.module'; import { MailerModule } from '../mailer/mailer.module'; import { UsersModule } from '../users/users.module'; import { Structure, StructureSchema } from './schemas/structure.schema'; @@ -18,7 +19,8 @@ import { StructureType, StructureTypeSchema } from './structure-type/structure-t ]), HttpModule, MailerModule, - UsersModule, + forwardRef(() => UsersModule), + TempUserModule, ], controllers: [StructuresController, StructureTypeController], exports: [StructuresService, StructureTypeService], diff --git a/src/tcl/tclStopPoint.controller.ts b/src/tcl/tclStopPoint.controller.ts index 2f8b75148e41fd90186763ecec5958e4c2d8c388..5f7e940c9e882afd7b4225dfdb92489f027e90d1 100644 --- a/src/tcl/tclStopPoint.controller.ts +++ b/src/tcl/tclStopPoint.controller.ts @@ -1,5 +1,8 @@ import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { Roles } from '../users/decorators/roles.decorator'; +import { RolesGuard } from '../users/guards/roles.guard'; import { PgisCoord } from './interfaces/pgis.coord'; import { TclStopPoint } from './tclStopPoint.schema'; import { TclStopPointService } from './tclStopPoint.service'; @@ -16,7 +19,8 @@ export class TclStopPointController { description: 'The stop points have been updated successfully.', }) @Get('/update') - //TODO: protect with admin guard when available + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') public updateStopPoints(): Promise<void> { return this.tclStopPointService.updateStopPoints(); } diff --git a/src/temp-user/dto/create-temp-user.dto.ts b/src/temp-user/dto/create-temp-user.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..94b61f14d79a02c1539e8dc775dd9391c0698263 --- /dev/null +++ b/src/temp-user/dto/create-temp-user.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsEmail, IsNotEmpty, IsOptional } from 'class-validator'; +import { Types } from 'mongoose'; + +export class CreateTempUserDto { + @IsNotEmpty() + @IsEmail() + @ApiProperty({ type: String }) + email: string; + + @IsArray() + @IsOptional() + pendingStructuresLink?: Types.ObjectId[]; +} diff --git a/src/temp-user/dto/temp-user-delete.dto.ts b/src/temp-user/dto/temp-user-delete.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac6149d3d67d378dc0fd6641652a636148f1782c --- /dev/null +++ b/src/temp-user/dto/temp-user-delete.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty } from 'class-validator'; + +export class DeleteTempUserDto { + @IsNotEmpty() + @IsEmail() + @ApiProperty({ type: String }) + email: string; +} diff --git a/src/temp-user/temp-user.controller.ts b/src/temp-user/temp-user.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..9dc21a559e179a4ba1fc59f6c70ff7b6e9a7dc01 --- /dev/null +++ b/src/temp-user/temp-user.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, HttpException, HttpStatus, Param } from '@nestjs/common'; +import { ApiParam } from '@nestjs/swagger'; +import { TempUser } from './temp-user.schema'; +import { TempUserService } from './temp-user.service'; + +@Controller('temp-user') +export class TempUserController { + constructor(private readonly tempUserSercice: TempUserService) {} + + @Get(':id') + @ApiParam({ name: 'id', type: String, required: true }) + public async getTempUser(@Param('id') id: string): Promise<TempUser> { + const user = await this.tempUserSercice.findById(id); + if (!user) { + throw new HttpException('User does not exists', HttpStatus.BAD_REQUEST); + } + return user; + } +} diff --git a/src/temp-user/temp-user.interface.ts b/src/temp-user/temp-user.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e0a132a1bb71e3fdd88ddc3bd5262e82e0c085e --- /dev/null +++ b/src/temp-user/temp-user.interface.ts @@ -0,0 +1,7 @@ +import { Document, Types } from 'mongoose'; + +export interface ITempUser extends Document { + readonly _id: string; + email: string; + pendingStructuresLink: Types.ObjectId[]; +} diff --git a/src/temp-user/temp-user.module.ts b/src/temp-user/temp-user.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b55281e49839559323398587a13a487cb462ea6 --- /dev/null +++ b/src/temp-user/temp-user.module.ts @@ -0,0 +1,14 @@ +import { HttpModule, Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { TempUser, TempUserSchema } from './temp-user.schema'; +import { TempUserService } from './temp-user.service'; +import { TempUserController } from './temp-user.controller'; +import { MailerModule } from '../mailer/mailer.module'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: TempUser.name, schema: TempUserSchema }]), HttpModule, MailerModule], + providers: [TempUserService], + exports: [TempUserService], + controllers: [TempUserController], +}) +export class TempUserModule {} diff --git a/src/temp-user/temp-user.schema.ts b/src/temp-user/temp-user.schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc70ce2ac22516550162e44b51d4d2523164aa82 --- /dev/null +++ b/src/temp-user/temp-user.schema.ts @@ -0,0 +1,15 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Types } from 'mongoose'; + +export type TempUserDocument = TempUser & Document; + +@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) +export class TempUser { + @Prop({ required: true }) + email: string; + + @Prop({ default: null }) + pendingStructuresLink: Types.ObjectId[]; +} + +export const TempUserSchema = SchemaFactory.createForClass(TempUser); diff --git a/src/temp-user/temp-user.service.ts b/src/temp-user/temp-user.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4588a4b9c199c84ea49152a2d4ad3e29630faf1 --- /dev/null +++ b/src/temp-user/temp-user.service.ts @@ -0,0 +1,87 @@ +import { HttpException, HttpService, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { MailerService } from '../mailer/mailer.service'; +import { CreateTempUserDto } from './dto/create-temp-user.dto'; +import { TempUser } from './temp-user.schema'; +import * as ejs from 'ejs'; +import { ITempUser } from './temp-user.interface'; + +@Injectable() +export class TempUserService { + constructor( + private readonly httpService: HttpService, + private readonly mailerService: MailerService, + @InjectModel(TempUser.name) private tempUserModel: Model<ITempUser> + ) {} + + public async create(createTempUser: CreateTempUserDto, structureName: string): Promise<TempUser> { + const userInDb = await this.findOne(createTempUser.email); + if (userInDb) { + throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); + } + const createUser = new this.tempUserModel(createTempUser); + // Send email + this.sendUserMail(createUser, structureName); + createUser.save(); + return await this.findOne(createTempUser.email); + } + + public async findOne(mail: string): Promise<TempUser | undefined> { + return this.tempUserModel.findOne({ email: mail }).exec(); + } + + public async findById(id: string): Promise<TempUser | undefined> { + return this.tempUserModel.findById(Types.ObjectId(id)).exec(); + } + + public async delete(mail: string): Promise<TempUser> { + const userInDb = await this.findOne(mail); + if (!userInDb) { + throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); + } + this.tempUserModel.deleteOne({ email: mail }).exec(); + return userInDb; + } + + public async updateStructureLinked(createTempUser: CreateTempUserDto): Promise<TempUser> { + const userInDb = await this.tempUserModel + .find({ + $and: [ + { + email: createTempUser.email, + }, + { + pendingStructuresLink: { $in: [createTempUser.pendingStructuresLink[0]] }, + }, + ], + }) + .exec(); + if (userInDb.length > 0) { + throw new HttpException('User already linked', HttpStatus.UNPROCESSABLE_ENTITY); + } + return this.tempUserModel + .updateOne( + { email: createTempUser.email }, + { $push: { pendingStructuresLink: createTempUser.pendingStructuresLink[0] } } + ) + .exec(); + } + + /** + * Send email in order to tell the user that an account is alreday fill with his structure info. + * @param user User + */ + private async sendUserMail(user: ITempUser, structureName: string): Promise<any> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.tempUserRegistration.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.tempUserRegistration.json); + + const html = await ejs.renderFile(ejsPath, { + config, + id: user._id, + name: structureName, + }); + this.mailerService.send(user.email, jsonConfig.subject, html); + } +} diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index db7bd07018e865772b44ffae7b5e8dd153529bfa..fd1b6680ffe47468672e367a5223603b4f84a058 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -27,4 +27,8 @@ export class CreateUserDto { @IsArray() @IsOptional() pendingStructuresLink?: Array<number>; + + @IsArray() + @IsOptional() + structuresLink?: Array<string>; } diff --git a/src/users/dto/owner.dto.ts b/src/users/dto/owner.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9f28ae9dd30becb286130968cc247996ffe8c09 --- /dev/null +++ b/src/users/dto/owner.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class OwnerDto { + @IsNotEmpty() + @IsEmail() + email: string; + + @IsNotEmpty() + @IsString() + id: string; +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 43fb564f56211d4f7fd7b0952d9237f8308d3bb4..4bde1ea1bf7a9f8d264dc36dc3f9360145c78e12 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Query, Request, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post, Query, Req, Request, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { PasswordChangeDto } from './dto/change-password.dto'; @@ -7,10 +7,16 @@ import { CreateUserDto } from './dto/create-user.dto'; import { PasswordResetApplyDto } from './dto/reset-password-apply.dto'; 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'; @Controller('users') export class UsersController { - constructor(private usersService: UsersService) {} + constructor( + private usersService: UsersService, + private structureService: StructuresService, + private tempUserService: TempUserService + ) {} @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT') @@ -33,7 +39,12 @@ export class UsersController { } const user = await this.usersService.create(createUserDto); if (structureId) { - this.usersService.updateStructureLinked(createUserDto.email, structureId); + this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId); + } + // Remove temp user if exist + const tempUser = await this.tempUserService.findOne(createUserDto.email); + if (tempUser) { + this.tempUserService.delete(createUserDto.email); } return user; } @@ -86,4 +97,23 @@ export class UsersController { public async resetPasswordApply(@Body() passwordResetApplyDto: PasswordResetApplyDto) { return this.usersService.validatePasswordResetToken(passwordResetApplyDto.password, passwordResetApplyDto.token); } + + @Post('verify-exist-user') + public async verifyUserExist(@Request() req, @Body() email: { newMail: string }) { + return this.usersService.verifyUserExist(email.newMail); + } + + @Delete() + @UseGuards(JwtAuthGuard) + public async delete(@Req() req) { + const user = await this.usersService.deleteOne(req.user.email); + user.structuresLink.forEach((structureId) => { + this.usersService.isStructureClaimed(structureId.toString()).then((userFound) => { + if (!userFound) { + this.structureService.deleteOne(structureId.toString()); + } + }); + }); + return user; + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index ae73bfe01263691656b98892941287ea85642e23..6a481a9e123a681074b40daac020f3b9244c2d65 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,12 +1,19 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, HttpModule, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { User, UserSchema } from './schemas/user.schema'; import { MailerModule } from '../mailer/mailer.module'; - +import { StructuresModule } from '../structures/structures.module'; +import { TempUserModule } from '../temp-user/temp-user.module'; @Module({ - imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), MailerModule], + imports: [ + MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), + MailerModule, + forwardRef(() => StructuresModule), + HttpModule, + TempUserModule, + ], providers: [UsersService], exports: [UsersService], controllers: [UsersController], diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index e24a4807c60a4d93623c6d6b03220032e34905b1..01f5207c753886b4a04644d506a7454f89ed52a4 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -7,6 +7,11 @@ import { CreateUserDto } from './dto/create-user.dto'; import { HttpException, HttpStatus } from '@nestjs/common'; import { LoginDto } from '../auth/login-dto'; import { EmailChangeDto } from './dto/change-email.dto'; +import * as bcrypt from 'bcrypt'; + +function hashPassword() { + return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT); +} describe('UsersService', () => { let service: UsersService; @@ -38,7 +43,7 @@ describe('UsersService', () => { 'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42', emailVerified: false, email: 'jacques.dupont@mii.com', - password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', + password: hashPassword(), newEmail: '', changeEmailToken: '', resetPasswordToken: null, @@ -74,7 +79,7 @@ describe('UsersService', () => { 'cf1c74c22cedb6b575945098db42d2f493fb759c9142c6aff7980f252886f36ee086574ee99a06bc99119079257116c959c8ec870949cebdef2b293666dbca42', emailVerified: false, email: 'jacques.dupont@mii.com', - password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', + password: hashPassword(), role: 0, newEmail: '', changeEmailToken: '', @@ -133,7 +138,7 @@ describe('UsersService', () => { validationToken: '', emailVerified: true, email: 'jacques.dupont@mii.com', - password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', + password: hashPassword(), role: 0, newEmail: 'test.dupont@mail.com', resetPasswordToken: '', @@ -162,7 +167,7 @@ describe('UsersService', () => { validationToken: '', emailVerified: true, email: 'test.dupont@mail.com', - password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', + password: hashPassword(), role: 0, newEmail: '', resetPasswordToken: '', diff --git a/src/users/users.service.ts b/src/users/users.service.ts index f683ebdca2a3a881a199c5b85964daf50557db6f..c7806f64e8ac23025e7aff33935f1fb1583e9adf 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -11,6 +11,7 @@ import { MailerService } from '../mailer/mailer.service'; import { IUser } from './interfaces/user.interface'; import { EmailChangeDto } from './dto/change-email.dto'; import { PendingStructureDto } from '../admin/dto/pending-structure.dto'; +import { OwnerDto } from './dto/owner.dto'; @Injectable() export class UsersService { @@ -32,6 +33,12 @@ export class UsersService { ); } let createUser = new this.userModel(createUserDto); + createUser.structuresLink = []; + if (createUserDto.structuresLink) { + createUserDto.structuresLink.forEach((structureId) => { + createUser.structuresLink.push(Types.ObjectId(structureId)); + }); + } // createUser.email = createUserDto.email; createUser.password = await this.hashPassword(createUser.password); // Send verification email @@ -80,6 +87,12 @@ export class UsersService { return this.userModel.findById(id).select('-password').exec(); } + public async removeStructureIdFromUsers(structureId: Types.ObjectId): Promise<IUser[] | undefined> { + return this.userModel + .updateMany({ structuresLink: { $in: [structureId] } }, { $pull: { structuresLink: structureId } }) + .exec(); + } + /** * Return a user after credential checking. * Use for login action @@ -306,6 +319,10 @@ export class UsersService { return this.userModel.findOne({ structuresLink: Types.ObjectId(structureId) }).exec(); } + public getStructureOwners(structureId: string): Promise<IUser[]> { + return this.userModel.find({ structuresLink: Types.ObjectId(structureId) }).exec(); + } + public async isUserAlreadyClaimedStructure(structureId: string, userEmail: string): Promise<boolean> { const user = await this.findOne(userEmail, true); if (user) { @@ -314,13 +331,18 @@ export class UsersService { return false; } - public async updateStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + public async updateStructureLinkedClaim(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure); + this.sendAdminStructureValidationMail(); + return stucturesLinked; + } + + public async updatePendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { const user = await this.findOne(userEmail, true); if (user) { if (!user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) { user.pendingStructuresLink.push(Types.ObjectId(idStructure)); - user.save(); - this.sendAdminStructureValidationMail(); + await user.save(); return user.pendingStructuresLink; } throw new HttpException('User already claimed this structure', HttpStatus.NOT_FOUND); @@ -328,6 +350,49 @@ export class UsersService { throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } + public async removeFromPendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const user = await this.findOne(userEmail, true); + if (user) { + if (user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) { + user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => { + return structureId === Types.ObjectId(idStructure); + }); + await user.save(); + return user.pendingStructuresLink; + } + throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + } + throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); + } + + public async updateStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const user = await this.findOne(userEmail, true); + if (user) { + if (!user.structuresLink.includes(Types.ObjectId(idStructure))) { + user.structuresLink.push(Types.ObjectId(idStructure)); + await user.save(); + return user.structuresLink; + } + throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + } + throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); + } + + public async removeFromStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const user = await this.findOne(userEmail, true); + if (user) { + if (user.structuresLink.includes(Types.ObjectId(idStructure))) { + user.structuresLink = user.structuresLink.filter((structureId) => { + return structureId == Types.ObjectId(idStructure); + }); + await user.save(); + return user.structuresLink; + } + throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + } + throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); + } + /** * Return all pending attachments of all profiles */ @@ -376,14 +441,14 @@ export class UsersService { status = true; // For other users who have made the demand on the same structure if (otherUsers) { - otherUsers.forEach((user) => { + otherUsers.forEach((otherUser) => { // Remove the structure id from their demand - user.pendingStructuresLink = user.pendingStructuresLink.filter((item) => { + otherUser.pendingStructuresLink = otherUser.pendingStructuresLink.filter((item) => { return !Types.ObjectId(structureId).equals(item); }); // Send a rejection email - this.sendStructureClaimApproval(user.email, structureName, false); - user.save(); + this.sendStructureClaimApproval(otherUser.email, structureName, false); + otherUser.save(); }); } } @@ -407,4 +472,31 @@ export class UsersService { user.save(); }); } + + public async verifyUserExist(email: string): Promise<boolean> { + const user = await this.findOne(email); + return user ? true : false; + } + + public async deleteOne(email: string): Promise<User> { + const user = await this.findOne(email); + if (!user) { + throw new HttpException('Invalid user email', HttpStatus.BAD_REQUEST); + } + return user.deleteOne(); + } + + public async getStructureOwnersMails(structureId: string, emailUser: string): Promise<OwnerDto[]> { + const users = await this.userModel + .find({ structuresLink: Types.ObjectId(structureId), email: { $ne: emailUser } }) + .exec(); + const owners: OwnerDto[] = []; + users.forEach((user) => { + const userProfile = new OwnerDto(); + userProfile.email = user.email; + userProfile.id = user._id; + owners.push(userProfile); + }); + return owners; + } } diff --git a/template.env b/template.env index fa2c9d0ae8b15dad61e8e27bf04059602c8c597c..54a211a951c1453d4215e6d28f3093d7026c279a 100644 --- a/template.env +++ b/template.env @@ -16,3 +16,4 @@ MAIL_TOKEN=<API token> APTIC_TOKEN=<APTIC API TOKEN> GHOST_PORT=<ghost port> GHOST_DB_PASSWORD=<ghost db password> +USER_PWD=<test user password>