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 (12)
Showing
with 148 additions and 158 deletions
...@@ -36,7 +36,6 @@ build_branch: ...@@ -36,7 +36,6 @@ build_branch:
image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/docker:18.09 image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/docker:18.09
stage: build stage: build
only: only:
- dev
- merge_requests - merge_requests
script: script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
......
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
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. 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.
## [2.2.0](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v2.1.3...v2.2.0) (2023-03-30)
### Features
* **structure:** disable aptic api ([#271](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/issues/271)) ([65a5ee5](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/65a5ee5ff3c1b13194d9af721f08874b47f1ca5b))
### Bug Fixes
* **newsletter:** add message for fake email ([3338103](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/33381037bed271fafe66618ca48412606dda3cc5))
* **newsletter:** subscription of unsubscribed email bug ([3b76a5d](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/3b76a5d80011765c43ce04352d4745efe4361bfa))
* **structure:** avoid date update in CTM script ([7e3f686](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/7e3f686121fca7a32a67c3267771d3b855b8a016))
* **structure:** delete structure by admin ([a69939f](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/a69939f8528bf9b0e3d002141424d90ad7ce7e0c))
### [2.1.3](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v2.1.2...v2.1.3) (2023-03-02) ### [2.1.3](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v2.1.2...v2.1.3) (2023-03-02)
......
...@@ -28,6 +28,9 @@ services: ...@@ -28,6 +28,9 @@ services:
ELASTICSEARCH_NODE: ${ELASTICSEARCH_NODE} ELASTICSEARCH_NODE: ${ELASTICSEARCH_NODE}
ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME} ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME}
ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD} ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD}
MC_API_KEY: ${MC_API_KEY}
MC_SERVER: ${MC_SERVER}
MC_LIST_ID: ${MC_LIST_ID}
restart: unless-stopped restart: unless-stopped
networks: networks:
- backend - backend
......
{ {
"name": "ram_server", "name": "ram_server",
"version": "2.1.3", "version": "2.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
......
{ {
"name": "ram_server", "name": "ram_server",
"private": true, "private": true,
"version": "2.1.3", "version": "2.2.0",
"description": "Nest TypeScript starter repository", "description": "Nest TypeScript starter repository",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
......
...@@ -5,7 +5,7 @@ module.exports = { ...@@ -5,7 +5,7 @@ module.exports = {
data: [ data: [
{ {
_id: mongoose.Types.ObjectId('6001a35f16b08100062e415f'), _id: mongoose.Types.ObjectId('6001a35f16b08100062e415f'),
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-16T15:37:00.000Z', createdAt: '2020-11-16T15:37:00.000Z',
updatedAt: '2021-03-02T12:36:53.000Z', updatedAt: '2021-03-02T12:36:53.000Z',
structureName: "Maison de l'Emploi (Feyzin)", structureName: "Maison de l'Emploi (Feyzin)",
...@@ -164,7 +164,7 @@ module.exports = { ...@@ -164,7 +164,7 @@ module.exports = {
handicaps: ['physicalDisability'], handicaps: ['physicalDisability'],
publicOthers: ['uniquementFemmes'], publicOthers: ['uniquementFemmes'],
}, },
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-16T10:19:00.000Z', createdAt: '2020-11-16T10:19:00.000Z',
updatedAt: '2020-12-16T10:19:00.000Z', updatedAt: '2020-12-16T10:19:00.000Z',
structureName: 'Pôle emploi (Vénissieux)', structureName: 'Pôle emploi (Vénissieux)',
...@@ -270,7 +270,7 @@ module.exports = { ...@@ -270,7 +270,7 @@ module.exports = {
}, },
{ {
_id: mongoose.Types.ObjectId('6001a38516b08100062e4161'), _id: mongoose.Types.ObjectId('6001a38516b08100062e4161'),
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-16T14:15:00.000Z', createdAt: '2020-11-16T14:15:00.000Z',
updatedAt: '2021-04-27T14:19:17.000Z', updatedAt: '2021-04-27T14:19:17.000Z',
structureName: 'Centre social Quartier Vitalité', structureName: 'Centre social Quartier Vitalité',
...@@ -371,7 +371,7 @@ module.exports = { ...@@ -371,7 +371,7 @@ module.exports = {
handicaps: ['physicalDisability'], handicaps: ['physicalDisability'],
publicOthers: ['uniquementFemmes'], publicOthers: ['uniquementFemmes'],
}, },
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-16T09:30:00.000Z', createdAt: '2020-11-16T09:30:00.000Z',
updatedAt: '2021-04-12T08:48:00.000Z', updatedAt: '2021-04-12T08:48:00.000Z',
structureName: "L'Atelier Numérique", structureName: "L'Atelier Numérique",
...@@ -514,7 +514,7 @@ module.exports = { ...@@ -514,7 +514,7 @@ module.exports = {
handicaps: ['physicalDisability'], handicaps: ['physicalDisability'],
publicOthers: ['uniquementFemmes'], publicOthers: ['uniquementFemmes'],
}, },
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-16T08:53:00.000Z', createdAt: '2020-11-16T08:53:00.000Z',
updatedAt: '2021-04-27T17:06:46.000Z', updatedAt: '2021-04-27T17:06:46.000Z',
structureName: 'Cyber-base / MJC Louis Aragon', structureName: 'Cyber-base / MJC Louis Aragon',
...@@ -618,7 +618,7 @@ module.exports = { ...@@ -618,7 +618,7 @@ module.exports = {
handicaps: ['physicalDisability'], handicaps: ['physicalDisability'],
publicOthers: ['uniquementFemmes'], publicOthers: ['uniquementFemmes'],
}, },
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-04T09:27:00.000Z', createdAt: '2020-11-04T09:27:00.000Z',
updatedAt: '2021-03-02T10:07:48.000Z', updatedAt: '2021-03-02T10:07:48.000Z',
structureName: 'Oasis Informatique', structureName: 'Oasis Informatique',
...@@ -730,7 +730,7 @@ module.exports = { ...@@ -730,7 +730,7 @@ module.exports = {
handicaps: ['physicalDisability'], handicaps: ['physicalDisability'],
publicOthers: ['uniquementFemmes'], publicOthers: ['uniquementFemmes'],
}, },
freeWorkShop: false, freeWorkShop: 'Non',
createdAt: '2020-11-13T14:13:00.000Z', createdAt: '2020-11-13T14:13:00.000Z',
updatedAt: '2021-03-02T10:09:30.000Z', updatedAt: '2021-03-02T10:09:30.000Z',
structureName: 'Le Son du Clic', structureName: 'Le Son du Clic',
...@@ -848,7 +848,7 @@ module.exports = { ...@@ -848,7 +848,7 @@ module.exports = {
pmrAccess: false, pmrAccess: false,
remoteAccompaniment: false, remoteAccompaniment: false,
accountVerified: true, accountVerified: true,
freeWorkShop: false, freeWorkShop: 'Non',
nbComputers: 0, nbComputers: 0,
nbPrinters: 0, nbPrinters: 0,
nbScanners: 0, nbScanners: 0,
...@@ -917,7 +917,7 @@ module.exports = { ...@@ -917,7 +917,7 @@ module.exports = {
pmrAccess: false, pmrAccess: false,
remoteAccompaniment: false, remoteAccompaniment: false,
accountVerified: true, accountVerified: true,
freeWorkShop: false, freeWorkShop: 'Non',
nbComputers: 0, nbComputers: 0,
nbPrinters: 0, nbPrinters: 0,
nbScanners: 0, nbScanners: 0,
......
...@@ -328,26 +328,6 @@ describe('AdminController', () => { ...@@ -328,26 +328,6 @@ describe('AdminController', () => {
}); });
}); });
describe('Search user newsletter subscription', () => {
it('should return all subscribed users, empty string', async () => {
expect((await controller.getNewsletterSubscriptions({ searchString: '' })).length).toBe(3);
});
it('should return all subscribed users, null input', async () => {
expect((await controller.getNewsletterSubscriptions({ searchString: null })).length).toBe(3);
});
it('should find one user', async () => {
expect((await controller.getNewsletterSubscriptions({ searchString: 'a@a.com' })).length).toBe(1);
expect((await controller.getNewsletterSubscriptions({ searchString: 'a@a.com' }))[0].email).toBe('a@a.com');
});
it('should find no user', async () => {
expect((await controller.getNewsletterSubscriptions({ searchString: 'adgdgsdg@a.com' })).length).toBe(0);
});
});
it('should count user subscribed to newsletter', async () => {
expect(await controller.countNewsletterSubscriptions()).toBe(246);
});
describe('Search delete a user subscription', () => { describe('Search delete a user subscription', () => {
it('should return a deleted object', async () => { it('should return a deleted object', async () => {
expect((await controller.unsubscribeUserFromNewsletter('a@a.com')).email).toBe('a@a.com'); expect((await controller.unsubscribeUserFromNewsletter('a@a.com')).email).toBe('a@a.com');
......
...@@ -251,24 +251,6 @@ export class AdminController { ...@@ -251,24 +251,6 @@ export class AdminController {
}); });
} }
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth('JWT')
@Post('searchNewsletterSubscriptions')
public async getNewsletterSubscriptions(@Body() searchString: { searchString: string }) {
if (searchString && searchString.searchString && searchString.searchString.length > 0)
return this.newsletterService.searchNewsletterSubscription(searchString.searchString);
else return this.newsletterService.findAll();
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth('JWT')
@Get('countNewsletterSubscriptions')
public async countNewsletterSubscriptions(): Promise<number> {
return this.newsletterService.countNewsletterSubscriptions();
}
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin') @Roles('admin')
@ApiBearerAuth('JWT') @ApiBearerAuth('JWT')
......
import { Db } from 'mongodb';
import { StructureDocument } from '../../structures/schemas/structure.schema';
import { getDb } from '../migrations-utils/db';
export const up = async () => {
const db: Db = await getDb();
const cursor = db.collection('structures').find({});
let document;
while ((document = await cursor.next())) {
const newDoc: StructureDocument = removeUnknownContactMail(document);
await db.collection('structures').updateOne({ _id: document._id }, [{ $set: newDoc }]);
}
console.log('Update done: Contact emails unknown@unknown.com emptied');
};
export const down = async () => {
// Nothing can be done since we can't know which null contactMail fields were previously filled with unknow@unknown.com
console.log('Downgrade done');
};
function removeUnknownContactMail(doc: StructureDocument): StructureDocument {
if (doc.contactMail && doc.contactMail === 'unknown@unknown.com') {
doc.contactMail = null;
}
return doc;
}
...@@ -51,24 +51,21 @@ describe('NewsletterService', () => { ...@@ -51,24 +51,21 @@ describe('NewsletterService', () => {
}); });
describe('newsletterSubscribe', () => { describe('newsletterSubscribe', () => {
it('it should not add subscription for email test2@test.com : already exist', async () => { it('it should add subscription for email test2@test.com even if it exists', async () => {
const result = { email: 'test2@test.com' } as INewsletterSubscription; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription;
mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
jest jest
.spyOn(service, 'findOne') .spyOn(service, 'findOne')
.mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => _doc);
try { mockNewsletterModel.create.mockResolvedValueOnce(_doc);
await service.newsletterSubscribe('test2@test.com');
// Fail test if above expression doesn't throw anything. const subscription = await service.newsletterSubscribe('test2@test.com');
expect(true).toBe(false); expect(subscription).toEqual(_doc);
} catch (e) {
expect(e.message).toEqual('Email already exists');
expect(e.status).toEqual(HttpStatus.BAD_REQUEST);
}
}); });
it('it should add a subscription for email test2@test.com', async () => { it('it should add a subscription for email test2@test.com', async () => {
const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; const result = { email: 'test2@test.com' } as INewsletterSubscription;
const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
mailchimp.lists.addListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' }); mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
jest jest
.spyOn(service, 'findOne') .spyOn(service, 'findOne')
.mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
...@@ -76,12 +73,12 @@ describe('NewsletterService', () => { ...@@ -76,12 +73,12 @@ describe('NewsletterService', () => {
mockNewsletterModel.create.mockResolvedValueOnce(_doc); mockNewsletterModel.create.mockResolvedValueOnce(_doc);
const subscription = await service.newsletterSubscribe('test2@test.com'); const subscription = await service.newsletterSubscribe('test2@test.com');
expect(subscription).toEqual({ email: 'test2@test.com' }); expect(subscription).toEqual(_doc);
}); });
it('it should return mailchimp 400 issue', async () => { it('it should return error if mailchimp 400 issue', async () => {
const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; const result = { email: 'test2@test.com' } as INewsletterSubscription;
const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 400 }); mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 400 });
jest jest
.spyOn(service, 'findOne') .spyOn(service, 'findOne')
.mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
...@@ -92,14 +89,14 @@ describe('NewsletterService', () => { ...@@ -92,14 +89,14 @@ describe('NewsletterService', () => {
await service.newsletterSubscribe('test2@test.com'); await service.newsletterSubscribe('test2@test.com');
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toBe('Email already exists'); expect(e.message).toBe('Subscribe error');
expect(e.status).toEqual(HttpStatus.BAD_REQUEST); expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
} }
}); });
it('it should return mailchimp 500 issue', async () => { it('it should return error if mailchimp 500 issue', async () => {
const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; const result = { email: 'test2@test.com' } as INewsletterSubscription;
const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 500 }); mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 500 });
jest jest
.spyOn(service, 'findOne') .spyOn(service, 'findOne')
.mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
...@@ -110,28 +107,27 @@ describe('NewsletterService', () => { ...@@ -110,28 +107,27 @@ describe('NewsletterService', () => {
await service.newsletterSubscribe('test2@test.com'); await service.newsletterSubscribe('test2@test.com');
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toBe('Server error'); expect(e.message).toBe('Subscribe error');
expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
} }
}); });
}); });
describe('newsletterUnsubscribe', () => { describe('newsletterUnsubscribe', () => {
it('it should not remove subscription for email test@test.com : does not exist', async () => { it('it should not remove subscription for email test@test.com : does not exist', async () => {
const result: INewsletterSubscription = undefined; mailchimp.lists.getListMember.mockRejectedValueOnce({ status: 404 });
jest
.spyOn(service, 'findOne')
.mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result);
try { try {
await service.newsletterUnsubscribe('test@test.com'); await service.newsletterUnsubscribe('test@test.com');
// Fail test if above expression doesn't throw anything. // Fail test if above expression doesn't throw anything.
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toEqual('Invalid email'); expect(e.message).toEqual('Email not found');
expect(e.status).toEqual(HttpStatus.BAD_REQUEST); expect(e.status).toEqual(HttpStatus.NOT_FOUND);
} }
}); });
it('it should remove a subscription for email test2@test.com', async () => { it('it should remove a subscription for email test2@test.com', async () => {
const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
mailchimp.lists.getListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
const result = { const result = {
email: 'test2@test.com', email: 'test2@test.com',
deleteOne: async () => _doc, deleteOne: async () => _doc,
...@@ -145,15 +141,6 @@ describe('NewsletterService', () => { ...@@ -145,15 +141,6 @@ describe('NewsletterService', () => {
}); });
}); });
describe('countNewsletterSubscriptions', () => {
it('it should count subscriptions', async () => {
mockNewsletterModel.countDocuments.mockResolvedValueOnce(69);
const count = await service.countNewsletterSubscriptions();
expect(count).toEqual(69);
});
});
describe('findOne', () => { describe('findOne', () => {
it('it should not find a subscription with email test@test.com', async () => { it('it should not find a subscription with email test@test.com', async () => {
mockNewsletterModel.findOne.mockResolvedValueOnce(undefined); mockNewsletterModel.findOne.mockResolvedValueOnce(undefined);
...@@ -175,18 +162,6 @@ describe('NewsletterService', () => { ...@@ -175,18 +162,6 @@ describe('NewsletterService', () => {
expect(findOneEmail).toEqual(_docs); expect(findOneEmail).toEqual(_docs);
}); });
}); });
describe('searchNewsletterSubscription', () => {
it('it should find 2 search result', async () => {
const _docs = [
{ _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription,
{ _id: 'bbbbb', email: 'test@test.com' } as INewsletterSubscription,
];
mockNewsletterModel.find.mockResolvedValueOnce(_docs);
const findOneEmail = await service.searchNewsletterSubscription('test');
expect(findOneEmail.length).toBe(2);
});
});
describe('updateNewsletterSubscription', () => { describe('updateNewsletterSubscription', () => {
it('should update existing user subscription', () => { it('should update existing user subscription', () => {
mailchimp.lists.getListMembersInfo.mockResolvedValueOnce({ total_items: 10 }).mockResolvedValueOnce({ mailchimp.lists.getListMembersInfo.mockResolvedValueOnce({ total_items: 10 }).mockResolvedValueOnce({
...@@ -198,10 +173,9 @@ describe('NewsletterService', () => { ...@@ -198,10 +173,9 @@ describe('NewsletterService', () => {
}); });
const result = { email: 'test2@test.com' } as INewsletterSubscription; const result = { email: 'test2@test.com' } as INewsletterSubscription;
const spyer = jest.spyOn(mockNewsletterModel, 'findOne'); const spyer = jest.spyOn(mockNewsletterModel, 'findOne');
// jest.spyOn(service, 'findOne').mockResolvedValueOnce(result).mockResolvedValueOnce(result);
mockNewsletterModel.findOne.mockResolvedValueOnce(result).mockResolvedValueOnce(null); mockNewsletterModel.findOne.mockResolvedValueOnce(result).mockResolvedValueOnce(null);
service.updateNewsletterSubscription(); service.updateNewsletterSubscription();
expect(spyer).toBeCalledTimes(2); expect(spyer).toBeCalledTimes(3);
// expect(spyerDelete).toBeCalledTimes(1); // expect(spyerDelete).toBeCalledTimes(1);
}); });
}); });
......
...@@ -2,11 +2,12 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; ...@@ -2,11 +2,12 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
import { Model } from 'mongoose'; import { Model } from 'mongoose';
import { md5 } from '../shared/utils';
import { IMailchimpSubscription } from './interface/mailchimp-subscription'; import { IMailchimpSubscription } from './interface/mailchimp-subscription';
import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; import { INewsletterSubscription } from './interface/newsletter-subscription.interface';
import { NewsletterSubscription, NewsletterSubscriptionDocument } from './newsletter-subscription.schema'; import { NewsletterSubscription } from './newsletter-subscription.schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires import mailchimp = require('@mailchimp/mailchimp_marketing');
const mailchimp = require('@mailchimp/mailchimp_marketing');
@Injectable() @Injectable()
export class NewsletterService { export class NewsletterService {
private readonly logger = new Logger(NewsletterService.name); private readonly logger = new Logger(NewsletterService.name);
...@@ -41,40 +42,64 @@ export class NewsletterService { ...@@ -41,40 +42,64 @@ export class NewsletterService {
} }
public async newsletterSubscribe(email: string): Promise<NewsletterSubscription> { public async newsletterSubscribe(email: string): Promise<NewsletterSubscription> {
this.logger.debug('newsletterSubscribe'); this.logger.debug(`newsletterSubscribe: ${email}`);
const existingEmail = await this.findOne(email); email = email.toLocaleLowerCase();
if (existingEmail) {
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
}
try { try {
const member = await mailchimp.lists.addListMember(this.LIST_ID, { // Add or update list member (to be able to subscribe again a member who had already subscribed then unsubscribed)
// (the second parameter is the MD5 hash of the lowercase email, we actually don't need to maintain a mapping in newsletterSubscription : cf. https://mailchimp.com/developer/marketing/docs/methods-parameters/#path-parameters )
const member = await mailchimp.lists.setListMember(this.LIST_ID, md5(email), {
email_address: email, email_address: email,
status_if_new: 'subscribed', // cf. https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/
status: 'subscribed', status: 'subscribed',
}); });
await this.newsletterSubscriptionModel.create({ email: email, mailchimpId: member.id });
return this.findOne(email); // We may not be aware that the user has unsubscribed from the newsletter, so it is ok if it already exists in newsletterSubscription
let newsletterSubscription = await this.findOne(email);
if (!newsletterSubscription) {
newsletterSubscription = await this.newsletterSubscriptionModel.create({
email: email,
mailchimpId: member.id,
});
}
return newsletterSubscription;
} catch (e) { } catch (e) {
if (e.status === 400) { if (e.status === 400 && e.response?.text?.includes('fake')) {
this.logger.error(`Error ${e.status}, user might already exist in mailchimplist`); throw new HttpException('Fake or invalid email', HttpStatus.I_AM_A_TEAPOT);
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
} else { } else {
this.logger.error(`Mailchimp configuration error`); this.logger.error(`newsletterSubscribe ${email}: ${JSON.stringify(e)}`);
throw new HttpException('Server error', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException('Subscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }
} }
public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> { public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> {
this.logger.debug('newsletterUnsubscribe'); this.logger.debug(`newsletterUnsubscribe: ${email}`);
const subscription = await this.findOne(email); email = email.toLocaleLowerCase();
if (!subscription) { const emailMd5Hashed = md5(email);
throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST);
let newsletterSubscription = await this.findOne(email);
if (newsletterSubscription) {
newsletterSubscription = newsletterSubscription.deleteOne();
} }
await mailchimp.lists.setListMember(this.LIST_ID, subscription.mailchimpId, {
email_address: email, try {
status: 'unsubscribed', const response = await mailchimp.lists.getListMember(this.LIST_ID, emailMd5Hashed);
}); if (response.status === 'unsubscribed') {
return subscription.deleteOne(); throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
}
await mailchimp.lists.setListMember(this.LIST_ID, emailMd5Hashed, {
status: 'unsubscribed',
});
} catch (e) {
if (e.status === 404) {
throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
} else {
this.logger.error(`newsletterUnsubscribe ${email}: ${JSON.stringify(e)}`);
throw new HttpException('Unsubscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
return newsletterSubscription;
} }
public async findOne(mail: string): Promise<INewsletterSubscription | undefined> { public async findOne(mail: string): Promise<INewsletterSubscription | undefined> {
...@@ -82,16 +107,6 @@ export class NewsletterService { ...@@ -82,16 +107,6 @@ export class NewsletterService {
return this.newsletterSubscriptionModel.findOne({ email: mail }); return this.newsletterSubscriptionModel.findOne({ email: mail });
} }
public async searchNewsletterSubscription(searchString: string): Promise<NewsletterSubscriptionDocument[]> {
this.logger.debug('searchNewsletterSubscription');
return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') });
}
public async countNewsletterSubscriptions(): Promise<number> {
this.logger.debug('countNewsletterSubscriptions');
return this.newsletterSubscriptionModel.countDocuments({});
}
public async findAll(): Promise<NewsletterSubscription[]> { public async findAll(): Promise<NewsletterSubscription[]> {
this.logger.debug('findAll'); this.logger.debug('findAll');
return this.newsletterSubscriptionModel.find(); return this.newsletterSubscriptionModel.find();
......
...@@ -4,6 +4,9 @@ import { Page } from '../pages/schemas/page.schema'; ...@@ -4,6 +4,9 @@ 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';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
export const md5 = (data: string): string => crypto.createHash('md5').update(data).digest('hex');
export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post { export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post {
// Handle image display. Rewrite image URL to fit ghost infra issue. // Handle image display. Rewrite image URL to fit ghost infra issue.
......
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Cron, CronExpression } from '@nestjs/schedule';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import * as https from 'https'; import * as https from 'https';
import * as _ from 'lodash'; import * as _ from 'lodash';
...@@ -80,7 +79,7 @@ export class ApticStructuresService { ...@@ -80,7 +79,7 @@ export class ApticStructuresService {
createdStructure.structureName = structure.name; createdStructure.structureName = structure.name;
createdStructure.contactPhone = structure.phone; createdStructure.contactPhone = structure.phone;
// Unkown fields (but mandatory) // Unkown fields (but mandatory)
createdStructure.contactMail = 'unknown@unknown.com'; createdStructure.contactMail = null;
createdStructure.categories.labelsQualifications = ['passNumerique']; createdStructure.categories.labelsQualifications = ['passNumerique'];
createdStructure.structureType = await this.structureTypeService.findByValue('autre'); createdStructure.structureType = await this.structureTypeService.findByValue('autre');
createdStructure.pmrAccess = false; createdStructure.pmrAccess = false;
...@@ -226,7 +225,6 @@ export class ApticStructuresService { ...@@ -226,7 +225,6 @@ export class ApticStructuresService {
/** /**
* Get Metropole new aptic structure evey week. For testing, please change the expression * Get Metropole new aptic structure evey week. For testing, please change the expression
*/ */
@Cron(CronExpression.EVERY_WEEK)
public getMetropoleMunicipality(): void { public getMetropoleMunicipality(): void {
const req = const req =
'https://download.data.grandlyon.com/ws/grandlyon/adr_voie_lieu.adrcomgl/all.json?maxfeatures=-1&start=1'; 'https://download.data.grandlyon.com/ws/grandlyon/adr_voie_lieu.adrcomgl/all.json?maxfeatures=-1&start=1';
......
...@@ -7,7 +7,7 @@ import * as ejs from 'ejs'; ...@@ -7,7 +7,7 @@ import * as ejs from 'ejs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose'; import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose';
import { lastValueFrom, map, Observable, tap } from 'rxjs'; import { lastValueFrom, map, tap } from 'rxjs';
import { PendingStructureDto } from '../../admin/dto/pending-structure.dto'; import { PendingStructureDto } from '../../admin/dto/pending-structure.dto';
import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto'; import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto';
import { Categories } from '../../categories/schemas/categories.schema'; import { Categories } from '../../categories/schemas/categories.schema';
...@@ -275,10 +275,6 @@ export class StructuresService { ...@@ -275,10 +275,6 @@ export class StructuresService {
.select('-_id -accountVerified -otherDescription -dataShareConsentDate') .select('-_id -accountVerified -otherDescription -dataShareConsentDate')
.exec() .exec()
).map((structure) => { ).map((structure) => {
// If structure has temp email, hide it
if (this.hasTempMail(structure)) {
structure.contactMail = null;
}
const repositoryKeys = categories.map((category) => category.id); const repositoryKeys = categories.map((category) => category.id);
repositoryKeys.forEach((el) => { repositoryKeys.forEach((el) => {
// Add referentiel // Add referentiel
...@@ -937,10 +933,6 @@ export class StructuresService { ...@@ -937,10 +933,6 @@ export class StructuresService {
this.mailerService.send(emailsObject, jsonConfig.subject, html); this.mailerService.send(emailsObject, jsonConfig.subject, html);
} }
private hasTempMail(structure: Structure): boolean {
return structure.contactMail === 'unknown@unknown.com';
}
public async getAllUserCompletedStructures(users: IUser[]) { public async getAllUserCompletedStructures(users: IUser[]) {
return Promise.all( return Promise.all(
users.map(async (user) => { users.map(async (user) => {
...@@ -1162,7 +1154,9 @@ export class StructuresService { ...@@ -1162,7 +1154,9 @@ export class StructuresService {
const structures: StructureDocument[] = await this.findAll(); const structures: StructureDocument[] = await this.findAll();
for (const structure of structures) { for (const structure of structures) {
this.setCtmTerritory(structure).then(async (updatedStructure) => { this.setCtmTerritory(structure).then(async (updatedStructure) => {
await this.structureModel.findByIdAndUpdate(new Types.ObjectId(structure._id), updatedStructure).exec(); await this.structureModel
.findByIdAndUpdate(new Types.ObjectId(structure._id), updatedStructure, { timestamps: false })
.exec();
}); });
} }
return null; return null;
......
...@@ -25,6 +25,7 @@ import { ITempUser } from '../temp-user/temp-user.interface'; ...@@ -25,6 +25,7 @@ import { ITempUser } from '../temp-user/temp-user.interface';
import { TempUser } from '../temp-user/temp-user.schema'; import { TempUser } from '../temp-user/temp-user.schema';
import { TempUserService } from '../temp-user/temp-user.service'; import { TempUserService } from '../temp-user/temp-user.service';
import { Roles } from '../users/decorators/roles.decorator'; import { Roles } from '../users/decorators/roles.decorator';
import { UserRole } from '../users/enum/user-role.enum';
import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard'; import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard';
import { RolesGuard } from '../users/guards/roles.guard'; import { RolesGuard } from '../users/guards/roles.guard';
import { pendingStructuresLink } from '../users/interfaces/pendingStructure'; import { pendingStructuresLink } from '../users/interfaces/pendingStructure';
...@@ -181,10 +182,10 @@ export class StructuresController { ...@@ -181,10 +182,10 @@ export class StructuresController {
description: `Mettre à jour les territoires CTM des structures à partir de Data Grand Lyon`, description: `Mettre à jour les territoires CTM des structures à partir de Data Grand Lyon`,
}) })
@ApiResponse({ @ApiResponse({
status: 204, status: 201,
description: 'The CTM territories have been updated successfully.', description: 'The CTM territories have been updated successfully.',
}) })
@Get('/ctm/update') @Post('/ctm/update')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin') @Roles('admin')
public async updateCTM(): Promise<void> { public async updateCTM(): Promise<void> {
...@@ -204,10 +205,10 @@ export class StructuresController { ...@@ -204,10 +205,10 @@ export class StructuresController {
const otherOwners: IUser[] = (await this.userService.getStructureOwners(id)).filter((owner) => { const otherOwners: IUser[] = (await this.userService.getStructureOwners(id)).filter((owner) => {
return !owner._id.equals(req.user._id); return !owner._id.equals(req.user._id);
}); });
if (otherOwners.length) { if (otherOwners.length === 0 || req.user.role === UserRole.admin) {
return this.structureService.setToBeDeleted(req.user, structure);
} else {
return this.structureService.deleteOne(structure); return this.structureService.deleteOne(structure);
} else {
return this.structureService.setToBeDeleted(req.user, structure);
} }
} }
......
import { Body, Controller, Get, Logger, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Logger, Post, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { Roles } from '../users/decorators/roles.decorator'; import { Roles } from '../users/decorators/roles.decorator';
...@@ -17,10 +17,10 @@ export class TclStopPointController { ...@@ -17,10 +17,10 @@ export class TclStopPointController {
description: `Mettre à jour les points d'arrêt TCL à partir de Data Grand Lyon`, description: `Mettre à jour les points d'arrêt TCL à partir de Data Grand Lyon`,
}) })
@ApiResponse({ @ApiResponse({
status: 204, status: 201,
description: 'The stop points have been updated successfully.', description: 'The stop points have been updated successfully.',
}) })
@Get('/update') @Post('/update')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin') @Roles('admin')
public updateStopPoints(): Promise<void> { public updateStopPoints(): Promise<void> {
......