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
Commits on Source (5)
Showing
with 9815 additions and 13511 deletions
...@@ -53,7 +53,7 @@ test: ...@@ -53,7 +53,7 @@ test:
- dev - dev
- merge_requests - merge_requests
needs: [] needs: []
image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:18.17.0 image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:20.18.0
services: services:
- name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/elasticsearch:7.16.2 - name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/elasticsearch:7.16.2
alias: elasticsearch alias: elasticsearch
......
18 20
ARG DEPENDENCY_PROXY= ARG DEPENDENCY_PROXY=
FROM ${DEPENDENCY_PROXY}node:18-bullseye FROM ${DEPENDENCY_PROXY}node:20.18-bullseye
# Create app directory # Create app directory
WORKDIR /app WORKDIR /app
......
This diff is collapsed.
...@@ -30,20 +30,19 @@ ...@@ -30,20 +30,19 @@
"@elastic/elasticsearch": "~8.5.0", "@elastic/elasticsearch": "~8.5.0",
"@gouvfr-anct/timetable-to-osm-opening-hours": "^1.1.0", "@gouvfr-anct/timetable-to-osm-opening-hours": "^1.1.0",
"@mailchimp/mailchimp_marketing": "^3.0.80", "@mailchimp/mailchimp_marketing": "^3.0.80",
"@nestjs/axios": "^1.0.0", "@nestjs/axios": "^3.1.0",
"@nestjs/common": "^9.0.11", "@nestjs/common": "^10.4.6",
"@nestjs/config": "^2.2.0", "@nestjs/config": "^3.3.0",
"@nestjs/core": "^9.0.11", "@nestjs/core": "^10.4.6",
"@nestjs/elasticsearch": "^9.0.0", "@nestjs/elasticsearch": "^10.0.2",
"@nestjs/jwt": "^9.0.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^9.2.1", "@nestjs/mongoose": "^10.1.0",
"@nestjs/passport": "^9.0.0", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^9.2.0", "@nestjs/platform-express": "^10.4.6",
"@nestjs/schedule": "^2.2.3", "@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^6.1.3", "@nestjs/swagger": "^7.4.2",
"@tryghost/admin-api": "^1.13.1", "@tryghost/admin-api": "^1.13.1",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"axios": "1.1.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
...@@ -58,6 +57,7 @@ ...@@ -58,6 +57,7 @@
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"puppeteer": "^23.4.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
...@@ -68,9 +68,9 @@ ...@@ -68,9 +68,9 @@
"devDependencies": { "devDependencies": {
"@compodoc/compodoc": "^1.1.24", "@compodoc/compodoc": "^1.1.24",
"@golevelup/ts-jest": "^0.3.2", "@golevelup/ts-jest": "^0.3.2",
"@nestjs/cli": "^9.1.3", "@nestjs/cli": "^10.4.5",
"@nestjs/schematics": "^9.0.3", "@nestjs/schematics": "^10.2.2",
"@nestjs/testing": "^9.0.11", "@nestjs/testing": "^10.4.6",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
......
...@@ -33,9 +33,9 @@ import { SetUserJobDto } from './dto/set-user-job.dto'; ...@@ -33,9 +33,9 @@ import { SetUserJobDto } from './dto/set-user-job.dto';
type PendingStructure = { type PendingStructure = {
userEmail: string; userEmail: string;
structureId: string; structureId: string;
createdAt: string; createdAt: Date;
structureName: string; structureName: string;
updatedAt: string; updatedAt: Date;
permalink: string; permalink: string;
lastUpdateMail: Date; lastUpdateMail: Date;
comment: string; comment: string;
......
...@@ -3,7 +3,7 @@ import { DateTime, Interval } from 'luxon'; ...@@ -3,7 +3,7 @@ import { DateTime, Interval } from 'luxon';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
public isDateOutdated(date: DateTime, nbMonths: number): boolean { public isDateOutdated(date: Date, nbMonths: number): boolean {
const today = DateTime.local().setZone('utc', { keepLocalTime: true }); const today = DateTime.local().setZone('utc', { keepLocalTime: true });
return Interval.fromDateTimes(date, today).length('months') > nbMonths; return Interval.fromDateTimes(date, today).length('months') > nbMonths;
} }
......
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { version } from '../package.json'; import { version } from '../package.json';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { ConfigurationModule } from './configuration/configuration.module';
describe('AppController', () => { describe('AppController', () => {
let appController: AppController; let appController: AppController;
beforeEach(async () => { beforeEach(async () => {
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
imports: [ConfigurationModule],
controllers: [AppController], controllers: [AppController],
}).compile(); }).compile();
appController = module.get<AppController>(AppController); appController = module.get<AppController>(AppController);
}); });
describe('healthcheck', () => { it('should return healthcheck', async () => {
it('should return healthcheck', async () => { const healthcheck = appController.healthcheck();
const healthcheck = appController.healthcheck(); expect(healthcheck.status).toBe('API Online');
expect(healthcheck.status).toBe('API Online'); expect(healthcheck.uptime).not.toBeNull();
expect(healthcheck.uptime).not.toBeNull(); expect(healthcheck.version).toBe(version);
expect(healthcheck.version).toBe(version);
});
}); });
}); });
import { Controller, Get } from '@nestjs/common'; import { Body, Controller, Get, Header, Logger, Post, Res } from '@nestjs/common';
import { version } from '../package.json'; import { version } from '../package.json';
import { Response } from 'express';
import { generatePDF } from './shared/utils';
import { GetPdfDto } from './users/dto/get-pdf.dto';
import { ConfigurationService } from './configuration/configuration.service';
@Controller() @Controller()
export class AppController { export class AppController {
private start = Date.now(); private start = Date.now();
private readonly logger = new Logger(AppController.name);
constructor(private configurationService: ConfigurationService) {}
@Get('healthcheck') @Get('healthcheck')
healthcheck() { healthcheck() {
...@@ -25,4 +32,21 @@ export class AppController { ...@@ -25,4 +32,21 @@ export class AppController {
matomoSiteId: process.env.MATOMO_SITEID, matomoSiteId: process.env.MATOMO_SITEID,
}; };
} }
@Post('/pdf')
@Header('Content-Type', 'application/pdf')
@Header('Cache-Control', 'no-cache, no-store, must-revalidate')
@Header('Pragma', 'no-cache')
@Header('Expires', '0')
async getPdf(@Body() body: GetPdfDto, @Res() res: Response): Promise<void> {
this.logger.log('getPdf: ' + JSON.stringify(body));
const buffer = await generatePDF(`${this.configurationService.appUrl}/${body.urlPath}`);
res.set({
'Content-Disposition': `attachment; filename=${body.fileName}`,
'Content-Length': buffer.length,
});
res.end(buffer);
}
} }
import { HttpModule, HttpService } from '@nestjs/axios'; import { HttpModule, HttpService } from '@nestjs/axios';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { AxiosResponse } from 'axios'; import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
...@@ -47,7 +47,7 @@ describe('MailerService', () => { ...@@ -47,7 +47,7 @@ describe('MailerService', () => {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {}, headers: {},
config: {}, config: {} as InternalAxiosRequestConfig,
}; };
httpServiceMock.post.mockImplementationOnce(() => of(result)); httpServiceMock.post.mockImplementationOnce(() => of(result));
expect(await mailerService.send('a@a.com', 'test', '<p>This is a test</p>')).toBe(result.data); expect(await mailerService.send('a@a.com', 'test', '<p>This is a test</p>')).toBe(result.data);
...@@ -65,7 +65,7 @@ describe('MailerService', () => { ...@@ -65,7 +65,7 @@ describe('MailerService', () => {
status: 400, status: 400,
statusText: 'KO', statusText: 'KO',
headers: {}, headers: {},
config: {}, config: {} as InternalAxiosRequestConfig,
}; };
httpServiceMock.post.mockImplementationOnce(() => throwError(result)); httpServiceMock.post.mockImplementationOnce(() => throwError(result));
try { try {
......
...@@ -9,7 +9,7 @@ export const up = async () => { ...@@ -9,7 +9,7 @@ export const up = async () => {
while ((document = await cursor.next())) { while ((document = await cursor.next())) {
let value: Date; let value: Date;
if (!document.structuresLink || document.structuresLink.length == 0) { if (!document.structuresLink || document.structuresLink.length == 0) {
value = DateTime.local(); value = DateTime.local().toJSDate();
} else { } else {
value = null; value = null;
} }
......
import { Db } from 'mongodb';
import { getDb } from '../migrations-utils/db';
export const up = async () => {
const db: Db = await getDb();
const usersCollection = db.collection('users');
const usersWithPendingLinks = await usersCollection
.find({
pendingStructuresLink: { $exists: true, $ne: [] },
})
.toArray();
for (const user of usersWithPendingLinks) {
const updatedLinks = user.pendingStructuresLink.map((link) => {
if (typeof link.createdAt === 'string') {
link.createdAt = new Date(link.createdAt);
}
return link;
});
await usersCollection.updateOne({ _id: user._id }, { $set: { pendingStructuresLink: updatedLinks } });
}
console.log('Update done: pending structures links createdAt field converted to ISODate');
};
export const down = async () => {
const db: Db = await getDb();
const usersCollection = db.collection('users');
const usersWithPendingLinks = await usersCollection
.find({
pendingStructuresLink: { $exists: true, $ne: [] },
})
.toArray();
for (const user of usersWithPendingLinks) {
const revertedLinks = user.pendingStructuresLink.map((link) => {
if (link.createdAt instanceof Date) {
link.createdAt = link.createdAt.toISOString();
}
return link;
});
await usersCollection.updateOne({ _id: user._id }, { $set: { pendingStructuresLink: revertedLinks } });
}
console.log('Downgrade done: pending structures links createdAt field converted to string');
};
import { HttpModule, HttpService } from '@nestjs/axios'; import { HttpModule, HttpService } from '@nestjs/axios';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { AxiosResponse } from 'axios'; import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ConfigurationModule } from '../configuration/configuration.module'; import { ConfigurationModule } from '../configuration/configuration.module';
import { PagesController } from './pages.controller'; import { PagesController } from './pages.controller';
...@@ -81,7 +81,7 @@ describe('PagesController', () => { ...@@ -81,7 +81,7 @@ describe('PagesController', () => {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {}, headers: {},
config: {}, config: {} as InternalAxiosRequestConfig,
}; };
httpServiceMock.get.mockImplementationOnce(() => of(axiosResult)); httpServiceMock.get.mockImplementationOnce(() => of(axiosResult));
const result = await (await pagesController.getPagebySlug('hello')).toPromise(); const result = await (await pagesController.getPagebySlug('hello')).toPromise();
......
import { HttpModule, HttpService } from '@nestjs/axios'; import { HttpModule, HttpService } from '@nestjs/axios';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { AxiosResponse } from 'axios'; import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ConfigurationModule } from '../configuration/configuration.module'; import { ConfigurationModule } from '../configuration/configuration.module';
import { PostsController } from './posts.controller'; import { PostsController } from './posts.controller';
...@@ -193,7 +193,7 @@ describe('PostsController', () => { ...@@ -193,7 +193,7 @@ describe('PostsController', () => {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {}, headers: {},
config: {}, config: {} as InternalAxiosRequestConfig,
}; };
jest.spyOn(httpServiceMock, 'get').mockImplementationOnce(() => of(result)); jest.spyOn(httpServiceMock, 'get').mockImplementationOnce(() => of(result));
const response = await postsController.findAll(query); const response = await postsController.findAll(query);
......
...@@ -4,6 +4,7 @@ import { Page } from '../pages/schemas/page.schema'; ...@@ -4,6 +4,7 @@ import { Page } from '../pages/schemas/page.schema';
import { Post } from '../posts/schemas/post.schema'; import { Post } from '../posts/schemas/post.schema';
import { UserRole } from '../users/enum/user-role.enum'; import { UserRole } from '../users/enum/user-role.enum';
import { User } from '../users/schemas/user.schema'; import { User } from '../users/schemas/user.schema';
import puppeteer from 'puppeteer';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto'); const crypto = require('crypto');
export const md5 = (data: string): string => crypto.createHash('md5').update(data).digest('hex'); export const md5 = (data: string): string => crypto.createHash('md5').update(data).digest('hex');
...@@ -80,3 +81,19 @@ export const sanitize = (str: string) => { ...@@ -80,3 +81,19 @@ export const sanitize = (str: string) => {
.replace(/\s/g, '-') // replace spaces by "-" .replace(/\s/g, '-') // replace spaces by "-"
.replace(/[^\w\s-]/g, ''); // remove all non alphanumeric characters except spaces and dashes .replace(/[^\w\s-]/g, ''); // remove all non alphanumeric characters except spaces and dashes
}; };
export async function generatePDF(url: string): Promise<Uint8Array> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle0' }); // wait until all is loaded
const buffer = await page.pdf({
format: 'A4',
printBackground: true, // Include background colors and images
margin: { top: '20mm', right: '10mm', bottom: '20mm', left: '10mm' },
});
await browser.close();
return buffer;
}
...@@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing'; ...@@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { DataGouvStructure } from '../interfaces/data-gouv-structure.interface'; import { DataGouvStructure } from '../interfaces/data-gouv-structure.interface';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { AxiosResponse } from 'axios'; import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { mockGouvStructure, mockGouvStructureToResinFormat } from '../../../test/mock/data/gouvStructures.mock.data'; import { mockGouvStructure, mockGouvStructureToResinFormat } from '../../../test/mock/data/gouvStructures.mock.data';
import { StructuresSearchService } from './structures-search.service'; import { StructuresSearchService } from './structures-search.service';
import { StructuresService } from './structures.service'; import { StructuresService } from './structures.service';
...@@ -79,7 +79,7 @@ describe('StructuresImportService', () => { ...@@ -79,7 +79,7 @@ describe('StructuresImportService', () => {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {}, headers: {},
config: {}, config: {} as InternalAxiosRequestConfig,
}; };
const saveSpy = jest const saveSpy = jest
......
...@@ -57,7 +57,8 @@ export class StructuresService { ...@@ -57,7 +57,8 @@ export class StructuresService {
filters?: Array<any>, filters?: Array<any>,
fields?: string[], fields?: string[],
onlyOffersWithAppointment?: boolean, onlyOffersWithAppointment?: boolean,
limit?: number limit?: number,
inseeCodes?: string[]
): Promise<StructureDocument[]> { ): Promise<StructureDocument[]> {
this.logger.debug( this.logger.debug(
`searchForStructures : ${text} | filters: ${JSON.stringify(filters)} | fields : ${JSON.stringify(fields)} | ` `searchForStructures : ${text} | filters: ${JSON.stringify(filters)} | fields : ${JSON.stringify(fields)} | `
...@@ -77,59 +78,80 @@ export class StructuresService { ...@@ -77,59 +78,80 @@ export class StructuresService {
return 'onlineProcedures' in filter || 'baseSkills' in filter || 'advancedSkills' in filter; return 'onlineProcedures' in filter || 'baseSkills' in filter || 'advancedSkills' in filter;
}); });
// we match ids from Elasticsearch with ids from mongoDB (and filters) and sort the result according to ElasticSearch order. // Construct a global $and condition that can encompass multiple $and and $or clauses
if (andFiltersNotOnOffers?.length > 0 && orFilters?.length == 0) { let queryConditions: FilterQuery<StructureDocument>[] = [
structures = await this.structureModel { deletedAt: { $exists: false } },
.find({ { accountVerified: true },
_id: { $in: ids }, ];
$and: [...this.parseFilter(andFiltersNotOnOffers), { deletedAt: { $exists: false }, accountVerified: true }], if (andFiltersNotOnOffers?.length) {
}) queryConditions.push({
.populate('personalOffers') $and: [...this.parseFilter(andFiltersNotOnOffers)],
.populate('structureType') });
.limit(limit) }
.exec(); if (orFilters?.length) {
} else if (andFiltersNotOnOffers?.length > 0 && orFilters?.length > 0) { queryConditions = this.addOrConditions(orFilters, queryConditions);
structures = await this.structureModel }
.find({ if (inseeCodes?.length > 0) {
_id: { $in: ids }, queryConditions.push({
$or: [...this.parseFilter(orFilters)], $or: [{ 'address.inseeCode': { $in: inseeCodes } }],
$and: [...this.parseFilter(andFiltersNotOnOffers), { deletedAt: { $exists: false }, accountVerified: true }], });
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
} else if (andFiltersNotOnOffers?.length == 0 && orFilters?.length > 0) {
structures = await this.structureModel
.find({
_id: { $in: ids },
$or: [...this.parseFilter(orFilters)],
$and: [{ deletedAt: { $exists: false }, accountVerified: true }],
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
} else {
structures = await this.structureModel
.find({
_id: { $in: ids },
$and: [{ deletedAt: { $exists: false }, accountVerified: true }],
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
} }
// Match ids from Elasticsearch text search with mongoDB query with filters
structures = await this.structureModel
.find({
_id: { $in: ids },
$and: queryConditions,
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
// Filter offers using structure offers and structure members personalOffers // Filter offers using structure offers and structure members personalOffers
if (andFiltersOnOffers.length) { if (andFiltersOnOffers.length) {
structures = await this.filterOnOffers(structures, andFiltersOnOffers, onlyOffersWithAppointment); structures = await this.filterOnOffers(structures, andFiltersOnOffers, onlyOffersWithAppointment);
} }
// Sort the result according to ElasticSearch order
return structures.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); return structures.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
} }
/**
* For "or" filters of different types, add to queryConditions a different "or" filter for each type
* (For example for "or" filters "ctm" and "freeWorkShop" : cf. issue #620 )
*/
private addOrConditions(
orFilters,
queryConditions: FilterQuery<StructureDocument>[]
): FilterQuery<StructureDocument>[] {
// Object to hold arrays for each key (type of filter)
const grouped = {};
orFilters.forEach((item) => {
// Get the key (assumes each item has only one key)
const key = Object.keys(item)[0];
const value = item[key];
// Initialize the array for the key if it doesn't exist
if (!grouped[key]) {
grouped[key] = [];
}
// Push the key-value pair into the array
grouped[key].push({ [key]: value });
});
// Create $or conditions for each group (type of filter)
for (const key in grouped) {
queryConditions.push({
$or: this.parseFilter(grouped[key]),
});
}
return queryConditions;
}
/** /**
* set structure offers and structure social workers personalOffers in non-persistant property structure.categoriesWithPersonalOffers * set structure offers and structure social workers personalOffers in non-persistant property structure.categoriesWithPersonalOffers
*/ */
...@@ -154,7 +176,8 @@ export class StructuresService { ...@@ -154,7 +176,8 @@ export class StructuresService {
await Promise.all( await Promise.all(
(structure.personalOffers || []).map(async (personalOffer) => { (structure.personalOffers || []).map(async (personalOffer) => {
if (!personalOffer.categories) { if (!personalOffer.categories) {
throw new Error(`personalOffer not populated for structure ${structure.structureName} : ${personalOffer}`); Logger.log(`personalOffer not populated for structure ${structure.structureName} : ${personalOffer}`);
return;
} }
// If we only want personalOffers from user with appointment // If we only want personalOffers from user with appointment
...@@ -627,7 +650,7 @@ export class StructuresService { ...@@ -627,7 +650,7 @@ export class StructuresService {
if (!structure) { if (!structure) {
throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST);
} }
structure.toBeDeletedAt = DateTime.local().plus({ weeks: 5 }).setZone('Europe/Paris').toString(); structure.toBeDeletedAt = DateTime.local().plus({ weeks: 5 }).setZone('Europe/Paris').toJSDate();
structure.save({ timestamps: !hasAdminRole(user) }); structure.save({ timestamps: !hasAdminRole(user) });
this.sendToBeDeletedNotification(user, structure); this.sendToBeDeletedNotification(user, structure);
...@@ -703,7 +726,7 @@ export class StructuresService { ...@@ -703,7 +726,7 @@ export class StructuresService {
} }
this.structuresSearchService.deleteIndexStructure(structure); this.structuresSearchService.deleteIndexStructure(structure);
if (structure.toBeDeletedAt) structure.toBeDeletedAt = null; if (structure.toBeDeletedAt) structure.toBeDeletedAt = null;
structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString(); structure.deletedAt = DateTime.local().setZone('Europe/Paris').toJSDate();
structure.save({ timestamps: !hasAdminRole(user) }); structure.save({ timestamps: !hasAdminRole(user) });
this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`); this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`);
...@@ -1285,7 +1308,7 @@ export class StructuresService { ...@@ -1285,7 +1308,7 @@ export class StructuresService {
const structures = await this.structureModel const structures = await this.structureModel
.find() .find()
.where('toBeDeletedAt') .where('toBeDeletedAt')
.lte(DateTime.local()) .lte(DateTime.local().toMillis())
.where('deletedAt') .where('deletedAt')
.exists(false) .exists(false)
.exec(); .exec();
......
...@@ -114,7 +114,8 @@ export class StructuresController { ...@@ -114,7 +114,8 @@ export class StructuresController {
body ? body.filters : null, body ? body.filters : null,
null, null,
body?.onlyOffersWithAppointment || false, body?.onlyOffersWithAppointment || false,
body?.limit || null body?.limit || null,
body?.inseeCodes || null
); );
} }
......
...@@ -48,6 +48,7 @@ describe('UsersController', () => { ...@@ -48,6 +48,7 @@ describe('UsersController', () => {
deleteOne: jest.fn(), deleteOne: jest.fn(),
findById: jest.fn(), findById: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
usersFilteredByJobAndLocation: jest.fn(),
isStructureClaimed: jest.fn(), isStructureClaimed: jest.fn(),
sendResetPasswordEmail: jest.fn(), sendResetPasswordEmail: jest.fn(),
updateDescription: jest.fn(), updateDescription: jest.fn(),
...@@ -147,6 +148,13 @@ describe('UsersController', () => { ...@@ -147,6 +148,13 @@ describe('UsersController', () => {
}); });
}); });
describe('findUsersWithJobWithPersonalOffer', () => {
it('should find users with job and with personal offer', async () => {
await usersController.findUsersWithJobWithPersonalOffer();
expect(userServiceMock.usersFilteredByJobAndLocation).toHaveBeenCalledTimes(1);
});
});
describe('setProfile', () => { describe('setProfile', () => {
const setProfileData: ProfileDto = { const setProfileData: ProfileDto = {
employerName: 'Metro', employerName: 'Metro',
...@@ -393,7 +401,7 @@ describe('UsersController', () => { ...@@ -393,7 +401,7 @@ describe('UsersController', () => {
}); });
it('should have expired token', async () => { it('should have expired token', async () => {
mockJwtService.decode.mockReturnValue({ expiresAt: '', idStructure: '', userId: '' }); mockJwtService.decode.mockReturnValue({ expiresAt: '2000-01-01T00:00:00.000Z', idStructure: '', userId: '' });
try { try {
await usersController.joinValidation('token', 'true'); await usersController.joinValidation('token', 'true');
expect(true).toBe(false); expect(true).toBe(false);
......
...@@ -60,6 +60,24 @@ export class UsersController { ...@@ -60,6 +60,24 @@ export class UsersController {
return req.user; return req.user;
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT')
@ApiOperation({ description: 'Find users with specific job criteria and location' })
@ApiResponse({ status: 200, description: 'Returns filtered list of users' })
@Get('filteredUsers')
public async findUsersWithJobWithPersonalOffer(
@Query('hasPersonalOffer') hasPersonalOffer?: boolean,
@Query('job') jobs?: string,
@Query('ctms') ctms?: string,
@Query('inseeCodes') inseeCodes?: string
): Promise<IUser[]> {
this.logger.log(`findUsersWithJobWithPersonalOffer: ${hasPersonalOffer}; ${jobs}; ${ctms}; ${inseeCodes}`);
const jobArray = jobs ? jobs.split(',') : undefined;
const ctmsArray = ctms ? ctms.split(',') : undefined;
const inseeCodesArray = inseeCodes ? inseeCodes.split(',') : undefined;
return this.usersService.usersFilteredByJobAndLocation(hasPersonalOffer, jobArray, ctmsArray, inseeCodesArray);
}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT') @ApiBearerAuth('JWT')
@ApiOperation({ description: 'Set user profile with employer and job' }) @ApiOperation({ description: 'Set user profile with employer and job' })
...@@ -269,7 +287,7 @@ export class UsersController { ...@@ -269,7 +287,7 @@ export class UsersController {
if (!token || !status) { if (!token || !status) {
throw new HttpException('Wrong parameters', HttpStatus.NOT_FOUND); throw new HttpException('Wrong parameters', HttpStatus.NOT_FOUND);
} }
if (decoded.expiresAt < today) { if (DateTime.fromISO(decoded.expiresAt) < today) {
throw new HttpException('Expired or invalid token', HttpStatus.FORBIDDEN); throw new HttpException('Expired or invalid token', HttpStatus.FORBIDDEN);
} }
// Get structure name // Get structure name
......