Skip to content
Snippets Groups Projects
Commit 16666ccd authored by Jérémie BRISON's avatar Jérémie BRISON
Browse files

Merge branch 'feat/add-user-to-structure' into 'dev'

Feat/add user to structure

See merge request web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server!34
parents af6ddecb 1fb80471
No related branches found
No related tags found
3 merge requests!38Recette,!37Dev,!34Feat/add user to structure
Showing
with 400 additions and 61 deletions
......@@ -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],
})
......
......@@ -7,31 +7,4 @@ export const configDev = {
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',
},
},
};
......@@ -7,31 +7,4 @@ export const configProd = {
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',
},
},
};
......@@ -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',
},
},
};
......@@ -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;
......
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.
{
"subject": "Un acteur demande a rejoindre votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon"
}
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.
{
"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"
}
......@@ -11,7 +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';
@Injectable()
export class StructuresService {
constructor(
......@@ -117,7 +119,7 @@ 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();
}
/**
......@@ -288,7 +290,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
*/
......@@ -305,6 +307,34 @@ 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());
......
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Param,
ParseIntPipe,
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 { TempUser } from '../temp-user/temp-user.schema';
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';
......@@ -15,7 +31,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 +74,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')
......@@ -100,4 +120,104 @@ export class StructuresController {
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);
}
}
import { 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';
......@@ -19,6 +20,7 @@ import { StructureType, StructureTypeSchema } from './structure-type/structure-t
HttpModule,
MailerModule,
UsersModule,
TempUserModule,
],
controllers: [StructuresController, StructureTypeController],
exports: [StructuresService, StructureTypeService],
......
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;
@IsNotEmpty()
@ApiProperty({ type: String })
name: string;
@IsNotEmpty()
@ApiProperty({ type: String })
surname: string;
@IsArray()
@IsOptional()
pendingStructuresLink?: Types.ObjectId[];
}
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty } from 'class-validator';
export class DeleteTempUserDto {
@IsNotEmpty()
@IsEmail()
@ApiProperty({ type: String })
email: string;
}
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;
}
}
import { Document, Types } from 'mongoose';
export interface ITempUser extends Document {
readonly _id: string;
email: string;
name: string;
surname: string;
pendingStructuresLink: Types.ObjectId[];
}
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 {}
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({ required: true })
name: string;
@Prop({ required: true })
surname: string;
@Prop({ default: null })
pendingStructuresLink: Types.ObjectId[];
}
export const TempUserSchema = SchemaFactory.createForClass(TempUser);
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, TempUserDocument } 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);
}
}
......@@ -7,10 +7,11 @@ 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 { TempUserService } from '../temp-user/temp-user.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
constructor(private usersService: UsersService, private tempUserService: TempUserService) {}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT')
......@@ -33,7 +34,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;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment