Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server
1 result
Show changes
Showing
with 368 additions and 197 deletions
......@@ -8,6 +8,7 @@ import * as _ from 'lodash';
import { DateTime } from 'luxon';
import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose';
import { map, Observable, tap } from 'rxjs';
import { PendingStructureDto } from '../../admin/dto/pending-structure.dto';
import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto';
import { Categories } from '../../categories/schemas/categories.schema';
import { Module } from '../../categories/schemas/module.class';
......@@ -144,10 +145,14 @@ export class StructuresService {
await this.getStructurePosition(createdStructure).then(async (position: StructureDocument) => {
return this.structuresSearchService.indexStructure(
await this.structureModel
.findByIdAndUpdate(new Types.ObjectId(createdStructure._id), {
address: position.address,
coord: position.coord,
})
.findByIdAndUpdate(
new Types.ObjectId(createdStructure._id),
{
address: position.address,
coord: position.coord,
},
{ new: true }
)
.populate('personalOffers')
.populate('structureType')
.exec()
......@@ -433,6 +438,7 @@ export class StructuresService {
const address = resbal.data.features[0];
if (address && address.geometry) {
structure.coord = address.geometry.coordinates;
structure.address.postcode = address.properties.postcode;
} else {
this.logger.error(
`No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`,
......@@ -447,6 +453,7 @@ export class StructuresService {
const address = res.data.features[0];
if (address && address.geometry) {
structure.coord = address.geometry.coordinates;
structure.address.postcode = address.properties.postcode;
} else {
this.logger.error(
`No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`,
......@@ -600,12 +607,16 @@ export class StructuresService {
public getCoord(
numero: string,
address: string,
zipcode: string,
commune: string,
scope: string
): Observable<AxiosResponse<PhotonResponse>> {
const req =
`https://download.data.grandlyon.com/geocoding/${scope}/api?q=` + numero + ' ' + address + ' ' + zipcode;
this.logger.debug('Print getCoord' + req);
`https://download.data.grandlyon.com/geocoding/${scope}/api?q=` +
(numero == null ? '' : numero + ' ') +
address +
' ' +
commune;
this.logger.debug('Print getCoord ' + req);
return this.httpService.get(encodeURI(req));
}
......@@ -702,6 +713,8 @@ export class StructuresService {
if (structure.toBeDeletedAt) structure.toBeDeletedAt = null;
structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString();
this.anonymizeStructure(structure).save();
this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`);
// Remove structure from owners (and check if there is a newly unattached user)
const owners = await this.getOwners(structure._id);
owners.forEach((owner) => {
......@@ -777,9 +790,17 @@ export class StructuresService {
.exists(false)
.exec();
const pendingStructures = (await this.userService.getPendingStructures()) as PendingStructureDto[];
const structurIds = pendingStructures.map((pending) => pending.structureId);
structures.forEach((structure) => {
this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`);
this.deleteOne(structure);
if (structurIds.includes(structure.id)) {
this.logger.debug(`cancel structure soft-delete : ${structure.structureName} (${structure._id})`);
this.structureModel.findByIdAndUpdate(new Types.ObjectId(structure.id), {
toBeDeletedAt: null,
});
} else {
this.deleteOne(structure);
}
});
}
......@@ -841,10 +862,14 @@ export class StructuresService {
}
/**
* Send an email to structure owners in order to accept or decline a join request
* Send an email to structure owners and admin in order to accept or decline a join request
* @param user User
*/
public async sendStructureJoinRequest(user: IUser, structure: StructureDocument): Promise<void> {
public async sendStructureJoinRequest(
user: IUser,
structure: StructureDocument,
validationToken: string
): 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);
......@@ -856,13 +881,18 @@ export class StructuresService {
surname: user.surname,
id: structure._id,
userId: user._id,
validationToken: validationToken,
});
const owners = await this.getOwners(structure._id);
owners.forEach((owner) => {
for (const owner of owners) {
if (!owner._id.equals(user._id)) {
this.mailerService.send(owner.email, jsonConfig.subject, html);
}
});
}
const admins = await this.userService.getAdmins();
for (const admin of admins) {
this.mailerService.send(admin.email, jsonConfig.subject, html);
}
}
private async getOwners(structureId: string): Promise<IUser[]> {
......
import { HttpService } from '@nestjs/axios';
import { HttpStatus } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
import { CategoriesServiceMock } from '../../test/mock/services/categories.mock.service';
import { HttpServiceMock } from '../../test/mock/services/http.mock.service';
import { StructuresServiceMock } from '../../test/mock/services/structures.mock.service';
......@@ -82,7 +83,7 @@ describe('AuthController', () => {
it('should update structure', async () => {
const structureService = new StructuresServiceMock();
const structure = structureService.findOne('6093ba0e2ab5775cfc01ed3e');
const structure = structureService.findOne(new Types.ObjectId('6093ba0e2ab5775cfc01ed3e'));
const structureId = '1';
const res = await controller.update(structureId, {
......@@ -246,44 +247,6 @@ describe('AuthController', () => {
expect(res).toBeTruthy();
});
it('should join user', async () => {
const userMock = new UsersServiceMock();
const user = userMock.findOne('pauline.dupont@mii.com');
let res = controller.join('6093ba0e2ab5775cfc01ed3e', {
phone: null,
resetPasswordToken: null,
changeEmailToken: null,
newEmail: null,
pendingStructuresLink: null,
structuresLink: null,
structureOutdatedMailSent: null,
personalOffers: null,
email: user.email,
name: user.name,
surname: user.surname,
emailVerified: true,
createdAt: new Date('2022-05-25T09:48:28.824Z'),
password: user.password,
validationToken: null,
role: null,
employer: {
name: 'test',
validated: true,
},
job: {
name: 'test',
validated: true,
hasPersonalOffer: false,
},
unattachedSince: null,
});
expect(res).toBeTruthy();
res = controller.join('', null);
expect(res).toBeTruthy();
res = controller.join('6093ba0e2ab5775cfc01ed3e', null);
expect(res).toBeTruthy();
});
it('should remove user from struct', async () => {
const res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', 'tsfsf6296');
expect(res).toBeTruthy();
......@@ -293,8 +256,4 @@ describe('AuthController', () => {
const res = controller.reportStructureError({ structureId: '6093ba0e2ab5775cfc01ed3e', content: null });
expect(res).toBeTruthy();
});
//TODO: test structure controler endpoint
//updateAccountVerified,
//updateStructureLinkedClaim
});
......@@ -26,6 +26,7 @@ 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 { pendingStructuresLink } from '../users/interfaces/pendingStructure';
import { IUser } from '../users/interfaces/user.interface';
import { User } from '../users/schemas/user.schema';
import { UsersService } from '../users/services/users.service';
......@@ -65,7 +66,8 @@ export class StructuresController {
.then((data) =>
data.filter(
(cityPoint) =>
cityPoint.properties.city?.toLowerCase().includes(city.toLowerCase()) &&
(cityPoint.properties.postcode == city ||
cityPoint.properties.city?.toLowerCase().includes(city.toLowerCase())) &&
cityPoint.properties.postcode.match(depRegex)
)
)
......@@ -135,7 +137,7 @@ export class StructuresController {
}
@Post(':id/claim')
public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> {
public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<pendingStructuresLink[]> {
const structure = await this.structureService.findOne(idStructure);
return this.userService.updateStructureLinkedClaim(user.email, idStructure, structure);
}
......@@ -209,7 +211,8 @@ export class StructuresController {
if (!structure) {
throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
}
user.pendingStructuresLink = [new Types.ObjectId(id)];
user.structuresLink = [new Types.ObjectId(id)];
// If user already exist, use created account
if (await this.userService.verifyUserExist(user.email)) {
this.tempUserService.sendUserMail(user as ITempUser, structure.structureName, true);
......@@ -224,24 +227,6 @@ export class StructuresController {
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 structure to user
const userFromDb = await this.userService.findOne(user.email);
if (!userFromDb) {
throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
}
await this.userService.updateStructureLinked(userFromDb.email, id);
// Send structure owners an email
this.structureService.sendStructureJoinRequest(userFromDb, structure);
}
@Delete(':id/owner/:userId')
@UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
@ApiParam({ name: 'id', type: String, required: true })
......@@ -279,7 +264,7 @@ export class StructuresController {
}
// Get temp user
const userFromDb = await this.tempUserService.findById(userId);
if (!userFromDb || !userFromDb.pendingStructuresLink.includes(new Types.ObjectId(id))) {
if (!userFromDb || !userFromDb.structuresLink.includes(new Types.ObjectId(id))) {
throw new HttpException('Invalid temp user', HttpStatus.NOT_FOUND);
}
this.tempUserService.removeFromStructureLinked(userFromDb.email, id);
......
import { HttpModule } from '@nestjs/axios';
import { forwardRef, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { CategoriesModule } from '../categories/categories.module';
import { MailerModule } from '../mailer/mailer.module';
......@@ -17,6 +18,8 @@ import { StructureTypeController } from './structure-type/structure-type.control
import { StructureType, StructureTypeSchema } from './structure-type/structure-type.schema';
import { StructureTypeService } from './structure-type/structure-type.service';
import { StructuresController } from './structures.controller';
import { config } from 'dotenv';
config();
@Module({
imports: [
......@@ -32,6 +35,10 @@ import { StructuresController } from './structures.controller';
CategoriesModule,
TempUserModule,
SearchModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '30d' }, // 1 month validity
}),
],
controllers: [StructuresController, StructureTypeController],
exports: [StructuresService, StructureTypeService],
......
......@@ -10,5 +10,5 @@ export class CreateTempUserDto {
@IsArray()
@IsOptional()
pendingStructuresLink?: Types.ObjectId[];
structuresLink?: Types.ObjectId[];
}
......@@ -2,5 +2,5 @@ import { Document, Types } from 'mongoose';
export interface ITempUser extends Document {
email: string;
pendingStructuresLink?: Types.ObjectId[];
structuresLink?: Types.ObjectId[];
}
......@@ -9,7 +9,7 @@ export class TempUser {
email: string;
@Prop({ default: null })
pendingStructuresLink?: Types.ObjectId[];
structuresLink?: Types.ObjectId[];
}
export const TempUserSchema = SchemaFactory.createForClass(TempUser);
......@@ -88,14 +88,14 @@ describe('TempUserService', () => {
describe('updateStructureLinked', () => {
it('should update structure linked', async () => {
const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] };
const tmpUser = { email: 'test2@test.com', structuresLink: [] };
tempUserModelMock.find.mockReturnThis();
tempUserModelMock.exec.mockResolvedValueOnce([]).mockResolvedValueOnce(tmpUser);
tempUserModelMock.findByIdAndUpdate.mockReturnThis();
expect(await service.updateStructureLinked(tmpUser)).toEqual(tmpUser);
});
it('should not update structure linked: User already linked', async () => {
const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] };
const tmpUser = { email: 'test2@test.com', structuresLink: [] };
tempUserModelMock.find.mockReturnThis();
tempUserModelMock.findByIdAndUpdate.mockReturnThis();
tempUserModelMock.exec.mockResolvedValueOnce([tmpUser]);
......
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import * as ejs from 'ejs';
import { Model, Types } from 'mongoose';
......@@ -9,6 +9,8 @@ import { TempUser } from './temp-user.schema';
@Injectable()
export class TempUserService {
private readonly logger = new Logger(TempUserService.name);
constructor(
private readonly mailerService: MailerService,
@InjectModel(TempUser.name) private tempUserModel: Model<ITempUser>
......@@ -20,6 +22,8 @@ export class TempUserService {
throw new HttpException('User already exists', HttpStatus.BAD_REQUEST);
}
const createUser = await this.tempUserModel.create(createTempUser);
this.logger.debug(`TempUsersService | tempUser created`);
// Send email
this.sendUserMail(createUser, structureName);
return this.findOne(createTempUser.email);
......@@ -50,18 +54,19 @@ export class TempUserService {
email: createTempUser.email,
},
{
pendingStructuresLink: { $in: [createTempUser.pendingStructuresLink[0]] },
structuresLink: { $in: [createTempUser.structuresLink[0]] },
},
],
})
.exec();
if (userInDb.length > 0) {
throw new HttpException('User already linked', HttpStatus.UNPROCESSABLE_ENTITY);
}
return this.tempUserModel
.findByIdAndUpdate(
{ email: createTempUser.email },
{ $push: { pendingStructuresLink: createTempUser.pendingStructuresLink[0] } }
{ $push: { structuresLink: new Types.ObjectId(createTempUser.structuresLink[0]) } }
)
.exec();
}
......@@ -89,23 +94,25 @@ export class TempUserService {
public async getStructureTempUsers(structureId: string): Promise<ITempUser[]> {
return this.tempUserModel
.find({ pendingStructuresLink: new Types.ObjectId(structureId) })
.find({ structuresLink: new Types.ObjectId(structureId) })
.select('email updatedAt')
.exec();
}
public async removeFromStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
const user = await this.findOne(userEmail);
this.logger.debug(`find user : ${JSON.stringify(user)}`);
if (!user) {
throw new HttpException('Invalid temp user', HttpStatus.NOT_FOUND);
}
if (!user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) {
if (!user.structuresLink.includes(new Types.ObjectId(idStructure))) {
throw new HttpException("Temp user doesn't belong to this structure", HttpStatus.NOT_FOUND);
}
user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => {
user.structuresLink = user.structuresLink.filter((structureId) => {
return !structureId.equals(idStructure);
});
await user.save();
return user.pendingStructuresLink;
return user.structuresLink;
}
}
import { HttpModule } from '@nestjs/axios';
import { HttpStatus } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
......@@ -69,6 +70,10 @@ describe('UsersController', () => {
delete: jest.fn(),
findOne: jest.fn(),
};
const mockJwtService = {
sign: jest.fn(),
decode: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
......@@ -98,6 +103,10 @@ describe('UsersController', () => {
provide: JobsService,
useValue: jobServiceMock,
},
{
provide: JwtService,
useValue: mockJwtService,
},
{
provide: getModelToken('TempUser'),
useValue: TempUser,
......@@ -252,7 +261,6 @@ describe('UsersController', () => {
const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete');
const createUserDto = new CreateUserDto();
createUserDto.pendingStructuresLink = [];
userServiceMock.create.mockResolvedValueOnce(usersMockData[0]);
const result = await controller.create(createUserDto);
......@@ -265,28 +273,6 @@ describe('UsersController', () => {
expect(result).toEqual(usersMockData[0]);
});
it('should create user with structure', async () => {
const userCreateSpyer = jest.spyOn(userServiceMock, 'create');
const structureFindOneSpyer = jest.spyOn(structureServiceMock, 'findOne');
const updateStructureLinkedClaimSpyer = jest.spyOn(userServiceMock, 'updateStructureLinkedClaim');
const sendAdminStructureNotificationSpyer = jest.spyOn(structureServiceMock, 'sendAdminStructureNotification');
const tempUserFindOneSpyer = jest.spyOn(tempUserServiceMock, 'findOne');
const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete');
const createUserDto = new CreateUserDto();
createUserDto.pendingStructuresLink = ['6093ba0e2ab5775cfc01fffe'];
userServiceMock.create.mockResolvedValueOnce(usersMockData[0]);
const result = await controller.create(createUserDto);
expect(userCreateSpyer).toBeCalledTimes(1);
expect(structureFindOneSpyer).toBeCalledTimes(1);
expect(updateStructureLinkedClaimSpyer).toBeCalledTimes(1);
expect(sendAdminStructureNotificationSpyer).toBeCalledTimes(1);
expect(tempUserFindOneSpyer).toBeCalledTimes(1);
expect(tempUserDeleteSpyer).toBeCalledTimes(0);
expect(result).toEqual(usersMockData[0]);
});
it('should create user with temp user', async () => {
const userCreateSpyer = jest.spyOn(userServiceMock, 'create');
const structureFindOneSpyer = jest.spyOn(structureServiceMock, 'findOne');
......@@ -296,7 +282,6 @@ describe('UsersController', () => {
const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete');
const createUserDto = new CreateUserDto();
createUserDto.pendingStructuresLink = [];
userServiceMock.create.mockResolvedValueOnce(usersMockData[0]);
tempUserServiceMock.findOne.mockResolvedValueOnce({ email: 'test@test.com', pendingStructuresLink: [] });
const result = await controller.create(createUserDto);
......
......@@ -13,25 +13,29 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { DateTime } from 'luxon';
import { Types } from 'mongoose';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { PasswordChangeDto } from '../dto/change-password.dto';
import { Structure } from '../../structures/schemas/structure.schema';
import { StructuresService } from '../../structures/services/structures.service';
import { TempUserService } from '../../temp-user/temp-user.service';
import { EmailChangeDto } from '../dto/change-email.dto';
import { PasswordChangeDto } from '../dto/change-password.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import { DescriptionDto } from '../dto/description.dto';
import { ProfileDto } from '../dto/profile.dto';
import { PasswordResetApplyDto } from '../dto/reset-password-apply.dto';
import { PasswordResetDto } from '../dto/reset-password.dto';
import { UsersService } from '../services/users.service';
import { StructuresService } from '../../structures/services/structures.service';
import { TempUserService } from '../../temp-user/temp-user.service';
import { ConfigurationService } from '../../configuration/configuration.service';
import { Structure } from '../../structures/schemas/structure.schema';
import { ProfileDto } from '../dto/profile.dto';
import { EmployerService } from '../services/employer.service';
import { JobsService } from '../services/jobs.service';
import { IUser } from '../interfaces/user.interface';
import { UpdateDetailsDto } from '../dto/update-details.dto';
import { IPendingStructureToken } from '../interfaces/pending-structure-token.interface';
import { IUser } from '../interfaces/user.interface';
import { User } from '../schemas/user.schema';
import { DescriptionDto } from '../dto/description.dto';
import { EmployerService } from '../services/employer.service';
import { JobsService } from '../services/jobs.service';
import { UsersService } from '../services/users.service';
@ApiTags('users')
@Controller('users')
export class UsersController {
......@@ -39,10 +43,10 @@ export class UsersController {
constructor(
private usersService: UsersService,
private structureService: StructuresService,
private tempUserService: TempUserService,
private tempUsersService: TempUserService,
private employerService: EmployerService,
private jobsService: JobsService,
private configurationService: ConfigurationService
private jwtService: JwtService
) {}
@UseGuards(JwtAuthGuard)
......@@ -95,27 +99,11 @@ export class UsersController {
@ApiResponse({ status: 201, description: 'User created' })
public async create(@Body() createUserDto: CreateUserDto) {
this.logger.debug('create');
// remove structureId for creation and add structure after
let structureId = null;
if (createUserDto.pendingStructuresLink.length > 0) {
structureId = createUserDto.pendingStructuresLink[0];
delete createUserDto.pendingStructuresLink;
}
const user = await this.usersService.create(createUserDto);
if (structureId) {
const structure = await this.structureService.findOne(structureId);
this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId, structure);
this.structureService.sendAdminStructureNotification(
null,
this.configurationService.config.templates.adminStructureClaim.ejs,
this.configurationService.config.templates.adminStructureClaim.json,
user
);
}
// Remove temp user if exist
const tempUser = await this.tempUserService.findOne(createUserDto.email);
const tempUser = await this.tempUsersService.findOne(createUserDto.email);
if (tempUser) {
this.tempUserService.delete(createUserDto.email);
this.tempUsersService.delete(createUserDto.email);
}
return user;
}
......@@ -123,7 +111,7 @@ export class UsersController {
@Post('verify/:id')
@ApiParam({ name: 'id', type: String, required: true })
@ApiResponse({ status: 201, description: 'User verified' })
@ApiResponse({ status: 401, description: "This token does'nt exist or is not associate to this user." })
@ApiResponse({ status: 401, description: "This token doesn't exist or is not associate to this user." })
public async validateUser(@Param() params, @Query('token') token: string) {
return this.usersService.validateUser(params.id, token);
}
......@@ -216,4 +204,100 @@ export class UsersController {
public async updateDescription(@Req() req, @Body() body: DescriptionDto): Promise<User> {
return this.usersService.updateDescription(req.user._id, body);
}
@Post('join-request/:id')
@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 structure to user
const userFromDb = await this.usersService.findOne(user.email);
if (!userFromDb) {
throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
}
// See structure to pending
const pendingStructures = await this.usersService.updatePendingStructureLinked(
userFromDb.email,
id,
structure.structureName
);
const token = pendingStructures
.filter((pending) => pending.id.equals(structure.id))
.map((pending) => pending.token);
// Send structure owners an email
this.structureService.sendStructureJoinRequest(userFromDb, structure, token[0]);
}
//add token in route + add infos in token
@Get('join-validate/:token/:status')
@ApiParam({ name: 'token', type: String, required: true })
@ApiParam({ name: 'status', type: String, required: true })
public async joinValidation(@Param('token') token: string, @Param('status') status: string): Promise<any> {
const decoded: IPendingStructureToken = this.jwtService.decode(token) as IPendingStructureToken;
const today = DateTime.local().setZone('utc', { keepLocalTime: true });
if (!token || !status) {
throw new HttpException('Wrong parameters', HttpStatus.NOT_FOUND);
}
if (decoded.expiresAt < today) {
throw new HttpException('Expired or invalid token', HttpStatus.FORBIDDEN);
}
// Get structure name
const structure = await this.structureService.findOne(decoded.idStructure);
if (!structure) {
throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
}
// Get user and add pending structure
const userFromDb = await this.usersService.findById(decoded.userId);
if (!userFromDb) {
throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
}
if (
!userFromDb.pendingStructuresLink
.map((pending) => pending.id)
.filter((id) => id.equals(new Types.ObjectId(decoded.idStructure)))
) {
throw new HttpException('User not linked to structure', HttpStatus.NOT_FOUND);
}
if (status == 'true') {
// Accept
await this.usersService.updateStructureLinked(userFromDb.email, decoded.idStructure);
await this.usersService.removeFromPendingStructureLinked(userFromDb.email, decoded.idStructure);
} else {
// Refuse
await this.usersService.removeFromPendingStructureLinked(userFromDb.email, decoded.idStructure);
}
await this.usersService.sendStructureClaimApproval(userFromDb.email, structure.structureName, status == 'true');
return { id: decoded.idStructure, name: structure.structureName };
}
// Cancel a user's join request
@Get('join-cancel/:idStructure/:idUser')
@ApiParam({ name: 'idStructure', type: String, required: true })
@ApiParam({ name: 'idUser', type: String, required: true })
public async joinCancel(@Param('idStructure') idStructure: string, @Param('idUser') idUser: string): Promise<any> {
// Get structure name
const structure = await this.structureService.findOne(idStructure);
if (!structure) {
throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
}
// Get user and add pending structure
const userFromDb = await this.usersService.findById(idUser);
if (!userFromDb) {
throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
}
if (
!userFromDb.pendingStructuresLink
.map((pending) => pending.id)
.filter((id) => id.equals(new Types.ObjectId(idStructure)))
) {
throw new HttpException('This structure is in pending state', HttpStatus.NOT_FOUND);
}
await this.usersService.removeFromPendingStructureLinked(userFromDb.email, idStructure);
}
}
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { Types } from 'mongoose';
import { PersonalOffer } from '../../personal-offers/schemas/personal-offer.schema';
export class CreateUserDto {
......@@ -27,11 +28,7 @@ export class CreateUserDto {
@IsArray()
@IsOptional()
pendingStructuresLink?: Array<string>;
@IsArray()
@IsOptional()
structuresLink?: Array<string>;
structuresLink?: Array<Types.ObjectId>;
@IsOptional()
unattachedSince?: Date;
......
export interface IPendingStructureToken {
userId: string;
idStructure: string;
expiresAt: string;
}
import { Types } from 'mongoose';
export interface pendingStructuresLink {
id: Types.ObjectId;
structureName: string;
token: string;
createdAt: string;
}
......@@ -4,6 +4,7 @@ import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-of
import { Employer } from './employer.schema';
import { Job } from './job.schema';
import { UserRole } from '../enum/user-role.enum';
import { pendingStructuresLink } from '../interfaces/pendingStructure';
@Schema({ timestamps: true })
export class User {
......@@ -47,7 +48,7 @@ export class User {
structuresLink: Types.ObjectId[];
@Prop({ default: null })
pendingStructuresLink: Types.ObjectId[];
pendingStructuresLink: pendingStructuresLink[];
@Prop({ default: null })
structureOutdatedMailSent: Types.ObjectId[];
......
import { IndicesRefreshResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/types';
import { Injectable, Logger } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { es_settings_homemade_french } from '../../shared/utils';
import { escapeElasticsearchQuery, es_settings_homemade_french } from '../../shared/utils';
import { Employer, EmployerDocument } from '../schemas/employer.schema';
@Injectable()
......@@ -85,7 +85,7 @@ export class EmployerSearchService {
query: {
query_string: {
analyze_wildcard: true,
query: searchString,
query: escapeElasticsearchQuery(searchString),
fields: ['name'],
fuzziness: 'AUTO',
},
......
import { IndicesCreateResponse, IndicesRefreshResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/types';
import { Injectable, Logger } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { es_settings_homemade_french } from '../../shared/utils';
import { escapeElasticsearchQuery, es_settings_homemade_french } from '../../shared/utils';
import { UserRegistrySearchBody } from '../interfaces/userRegistry-search-body.interface';
import { IUserRegistry } from '../interfaces/userRegistry.interface';
......@@ -88,7 +88,7 @@ export class UserRegistrySearchService {
query: {
query_string: {
analyze_wildcard: true,
query: searchString,
query: escapeElasticsearchQuery(searchString),
fields: ['name', 'surname'],
fuzziness: 'AUTO',
},
......
......@@ -22,8 +22,9 @@ export class UserRegistryService {
public maxPerPage = 20;
public async findAllForIndexation(): Promise<IUserRegistry[]> {
this.logger.debug('findAllForIndexation');
return this.userModel
.find()
.find({ email: { $ne: process.env.MAIL_CONTACT } })
.where('emailVerified')
.equals(true)
.select('name surname _id job employer ')
......@@ -33,8 +34,9 @@ export class UserRegistryService {
}
public async countAllUserRegistry(): Promise<number> {
this.logger.debug('countAllUserRegistry');
return this.userModel
.find()
.find({ email: { $ne: process.env.MAIL_CONTACT } })
.where('emailVerified')
.equals(true)
.populate('employer')
......@@ -46,10 +48,11 @@ export class UserRegistryService {
}
public async findAllUserRegistry(page: number): Promise<UserRegistryPaginatedResponse> {
this.logger.debug('findAllUserRegistry');
const limit = this.maxPerPage * page;
const count = await this.countAllUserRegistry();
const docs = await this.userModel
.find()
.find({ email: { $ne: process.env.MAIL_CONTACT } })
.where('emailVerified')
.equals(true)
.populate('employer')
......@@ -62,6 +65,7 @@ export class UserRegistryService {
}
private callbackFilter(users: IUser[], employersList: Employer[], jobList: Job[]): IUser[] {
this.logger.debug('callbackFilter');
const jobNames: string[] = jobList.map((job) => job.name);
const employersNames: string[] = employersList.map((e) => e.name);
// For each filter list (job or employer), we'll filter the main user list in order to get only the user that have a job or employer contained in the filters array
......@@ -89,6 +93,7 @@ export class UserRegistryService {
jobs?: string[],
employers?: string[]
): Promise<UserRegistryPaginatedResponse> {
this.logger.debug('findUsersByNameEmployerOrJob');
const results = await this.userRegistrySearchService.search(searchParam);
const ids = results.map((result) => result.id);
const limit = page * this.maxPerPage || this.maxPerPage;
......@@ -107,6 +112,7 @@ export class UserRegistryService {
const resultsWithFilter = await this.userModel
.find({
_id: { $in: ids },
email: { $ne: process.env.MAIL_CONTACT },
})
.where('emailVerified')
.equals(true)
......
import { HttpModule } from '@nestjs/axios';
import { HttpException, HttpStatus } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import * as bcrypt from 'bcrypt';
......@@ -72,13 +73,18 @@ const mockUserRegistrySearchService = {
update: jest.fn(),
};
const mockJwtService = {
sign: jest.fn(),
decode: jest.fn(),
};
const createUserDto: CreateUserDto = {
email: 'jacques.dupont@mii.com',
password: 'test1A!!',
name: 'Jacques',
surname: 'Dupont',
phone: '06 06 06 06 06',
structuresLink: ['61e9260c2ac971550065e262', '61e9260b2ac971550065e261'],
structuresLink: [new Types.ObjectId('61e9260c2ac971550065e262'), new Types.ObjectId('61e9260b2ac971550065e261')],
};
const mockUser: User = {
......@@ -124,6 +130,10 @@ describe('UsersService', () => {
provide: UserRegistrySearchService,
useValue: mockUserRegistrySearchService,
},
{
provide: JwtService,
useValue: mockJwtService,
},
{
provide: getModelToken('User'),
useValue: mockUserModel,
......
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Cron, CronExpression } from '@nestjs/schedule';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import * as ejs from 'ejs';
......@@ -16,6 +18,7 @@ import { EmailChangeDto } from '../dto/change-email.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import { DescriptionDto } from '../dto/description.dto';
import { UpdateDetailsDto } from '../dto/update-details.dto';
import { pendingStructuresLink } from '../interfaces/pendingStructure';
import { IUser } from '../interfaces/user.interface';
import { IUserRegistry } from '../interfaces/userRegistry.interface';
import { EmployerDocument } from '../schemas/employer.schema';
......@@ -30,7 +33,8 @@ export class UsersService {
@InjectModel(User.name) private userModel: Model<IUser>,
private readonly mailerService: MailerService,
private userRegistrySearchService: UserRegistrySearchService,
private configurationService: ConfigurationService
private configurationService: ConfigurationService,
private jwtService: JwtService
) {}
/**
......@@ -49,12 +53,7 @@ export class UsersService {
);
}
let createUser = new this.userModel(createUserDto);
createUser.structuresLink = [];
if (createUserDto.structuresLink) {
createUserDto.structuresLink.forEach((structureId) => {
createUser.structuresLink.push(new Types.ObjectId(structureId));
});
}
createUser.structuresLink = createUser.structuresLink.map((id) => new Types.ObjectId(id));
createUser.password = await this.hashPassword(createUser.password);
createUser.unattachedSince = DateTime.local();
// Send verification email
......@@ -257,7 +256,7 @@ export class UsersService {
* a new account.
* @param user User
*/
private async sendStructureClaimApproval(userEmail: string, structureName: string, status: boolean): Promise<any> {
public async sendStructureClaimApproval(userEmail: string, structureName: string, status: boolean): Promise<any> {
const config = this.mailerService.config;
const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureClaimValidation.ejs);
const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureClaimValidation.json);
......@@ -462,7 +461,7 @@ export class UsersService {
public async isUserAlreadyClaimedStructure(structureId: string, userEmail: string): Promise<boolean> {
const user = await this.findOne(userEmail, true);
if (user) {
return user.pendingStructuresLink.includes(new Types.ObjectId(structureId));
return user.pendingStructuresLink.map((pending) => pending.id).includes(new Types.ObjectId(structureId));
}
return false;
}
......@@ -496,18 +495,49 @@ export class UsersService {
userEmail: string,
idStructure: string,
structure: StructureDocument
): Promise<Types.ObjectId[]> {
const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure);
): Promise<pendingStructuresLink[]> {
const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure, structure.structureName);
this.sendAdminStructureValidationMail(userEmail, structure);
return stucturesLinked;
}
public async updatePendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
/**
* Creates a 1 month valid token for a pending structure request
* @param user
* @param idStructure
* @returns
*/
private createPendingToken(user: IUser, idStructure: string): { token: string; createdAt: string } {
const local = DateTime.local().setZone('Europe/Paris');
return {
token: this.jwtService.sign({ userId: user.id, idStructure: idStructure, expiresAt: local.plus({ month: 1 }) }),
createdAt: local,
};
}
/**
* Updates the array of user's pending structures
* @param userEmail
* @param idStructure
* @returns
*/
public async updatePendingStructureLinked(
userEmail: string,
idStructure: string,
structureName: string
): Promise<pendingStructuresLink[]> {
const user = await this.findOne(userEmail, true);
if (user) {
if (!user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) {
user.pendingStructuresLink.push(new Types.ObjectId(idStructure));
if (!user.pendingStructuresLink.map((pending) => pending.id).includes(new Types.ObjectId(idStructure))) {
const { token, createdAt } = this.createPendingToken(user, idStructure);
user.pendingStructuresLink.push({
id: new Types.ObjectId(idStructure),
token: token,
createdAt: createdAt,
structureName: structureName,
});
await user.save();
return user.pendingStructuresLink;
}
......@@ -516,17 +546,30 @@ export class UsersService {
throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
}
public async removeFromPendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
/**
* Removes a strcture from the users's pending list
* @param userEmail
* @param idStructure
* @returns
*/
public async removeFromPendingStructureLinked(
userEmail: string,
idStructure: string
): Promise<pendingStructuresLink[]> {
const user = await this.findOne(userEmail, true);
if (user) {
if (user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) {
user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => {
return structureId === new Types.ObjectId(idStructure);
if (
user.pendingStructuresLink
.map((pending) => pending.id)
.filter((id) => id.equals(new Types.ObjectId(idStructure)))
) {
user.pendingStructuresLink = user.pendingStructuresLink.filter((pending) => {
return !pending.id.equals(new Types.ObjectId(idStructure));
});
await user.save();
return user.pendingStructuresLink;
}
throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);
throw new HttpException('User already belong to this structure', HttpStatus.UNPROCESSABLE_ENTITY);
}
throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
}
......@@ -540,7 +583,7 @@ export class UsersService {
await user.save();
return user.structuresLink;
}
throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);
throw new HttpException('User already belong to this structure', HttpStatus.UNPROCESSABLE_ENTITY);
}
throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
}
......@@ -565,19 +608,42 @@ export class UsersService {
/**
* Return all pending attachments of all profiles
*/
public async getPendingStructures(): Promise<PendingStructureDto[]> {
const users = await this.userModel.find();
const structuresPending = [];
// For each user, if they have structures in pending, push them in tab and return this tab.
users.forEach((user) => {
if (user.pendingStructuresLink.length) {
user.pendingStructuresLink.forEach((structureId) => {
structuresPending.push({ userEmail: user.email, structureId: structureId });
});
public async getPendingStructures(returnUsers = false): Promise<PendingStructureDto[] | IUser[]> {
const users = await this.userModel.find({ 'pendingStructuresLink.0': { $exists: true } }).exec();
if (!users.length) {
return [];
}
if (returnUsers) {
return users;
} else {
let structuresPending = [];
for (const user of users) {
structuresPending = structuresPending.concat(
this.parsePendingStructureLinkToStructureDto(user.email, user.pendingStructuresLink)
);
}
});
return structuresPending;
return structuresPending as PendingStructureDto[];
}
}
/**
* parsePendingStructureLinkToStructureDto
*/
public parsePendingStructureLinkToStructureDto(
userEmail: string,
pendingStructureLink: pendingStructuresLink[]
): PendingStructureDto[] {
const pendingStructureDto: PendingStructureDto[] = [];
for (const pendingLink of pendingStructureLink) {
const strDto: PendingStructureDto = {
userEmail: userEmail,
structureId: pendingLink.id,
createdAt: new Date(pendingLink.createdAt),
structureName: pendingLink.structureName,
};
pendingStructureDto.push(strDto);
}
return pendingStructureDto;
}
/**
......@@ -591,17 +657,21 @@ export class UsersService {
): Promise<PendingStructureDto[]> {
const user = await this.findOne(userEmail);
// Get other users who have made the demand on the same structure
const otherUsers = await this.userModel
.find({ pendingStructuresLink: new Types.ObjectId(structureId), email: { $ne: userEmail } })
.find({ 'pendingStructuresLink.id': { $eq: new Types.ObjectId(structureId) }, email: { $ne: userEmail } })
.exec();
let status = false;
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (user.pendingStructuresLink.includes(new Types.ObjectId(structureId))) {
if (
user.pendingStructuresLink.map((pending) => pending.id).filter((id) => id.equals(new Types.ObjectId(structureId)))
.length
) {
user.pendingStructuresLink = user.pendingStructuresLink.filter((item) => {
return !new Types.ObjectId(structureId).equals(item);
return !new Types.ObjectId(structureId).equals(item.id);
});
// If it's a validation case, push structureId into validated user structures
if (validate) {
......@@ -613,7 +683,7 @@ export class UsersService {
otherUsers.forEach((otherUser) => {
// Remove the structure id from their demand
otherUser.pendingStructuresLink = otherUser.pendingStructuresLink.filter((item) => {
return !new Types.ObjectId(structureId).equals(item);
return !new Types.ObjectId(structureId).equals(item.id);
});
// Send a rejection email
this.sendStructureClaimApproval(otherUser.email, structureName, false);
......@@ -623,7 +693,7 @@ export class UsersService {
}
this.sendStructureClaimApproval(userEmail, structureName, status);
await user.save();
return this.getPendingStructures();
return (await this.getPendingStructures()) as PendingStructureDto[];
} else {
throw new HttpException(
'Cannot validate strucutre. It might have been already validate, or the structure doesn`t belong to the user',
......@@ -812,4 +882,21 @@ export class UsersService {
.populate('personalOffers')
.exec();
}
@Cron(CronExpression.EVERY_DAY_AT_3AM)
public async cleanPendingStructures(): Promise<void> {
this.logger.debug('pendingStructuresCleaning process');
const users: IUser[] = (await this.getPendingStructures(true)) as IUser[];
for (const user of users) {
for (const [index, pending] of user.pendingStructuresLink.entries()) {
try {
this.jwtService.verify(pending.token);
} catch (e) {
this.logger.debug(`Expired token for structure ${pending.structureName}`);
user.pendingStructuresLink.splice(index, 1);
this.userModel.findByIdAndUpdate({ _id: user.id }, user).exec();
}
}
}
}
}