Commit b62b9c04 authored by Nicolas Castejon's avatar Nicolas Castejon
Browse files

Merge branch 'development' into 'master'

Version 1.2.1

See merge request refonte-data/service-email!3
parents 71c198db 45ebb742
Pipeline #2445 passed with stage
in 9 seconds
......@@ -4,6 +4,8 @@ stages:
variables:
USER_SUPPORT_MAILBOX: alpha-test@erasme.org
SMTP_HOST: mail0.erasme.org
SMTP_PORT: 25
# ADDITIONAL_FEEDBACK_EMAILS:
build_development:
......@@ -13,8 +15,6 @@ build_development:
script:
- export TAG=dev
- export MAIL_SERVICE_BIND_PORT=3000
- export RABBITMQ_LISTENING_PORT=5672
- export RABBITMQ_GUI_PORT=15672
- docker-compose build
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker-compose push
......@@ -28,8 +28,6 @@ build_release:
script:
- export TAG=$(echo $CI_COMMIT_TAG | sed 's/v//g')
- export MAIL_SERVICE_BIND_PORT=3000
- export RABBITMQ_LISTENING_PORT=5672
- export RABBITMQ_GUI_PORT=15672
- docker-compose build
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker-compose push
......@@ -41,8 +39,6 @@ deploy_development:
script:
- export TAG=dev
- export MAIL_SERVICE_BIND_PORT=3001
- export RABBITMQ_LISTENING_PORT=5672
- export RABBITMQ_GUI_PORT=15672
- export MAIL_SUBJECT_PREFIX=alpha
- export NO_REPLY_MAIL_ADDRESS=no-reply@erasme.org
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
......@@ -59,8 +55,6 @@ deploy_staging:
script:
- export TAG=staging
- export MAIL_SERVICE_BIND_PORT=3101
- export RABBITMQ_LISTENING_PORT=5673
- export RABBITMQ_GUI_PORT=15673
- export MAIL_SUBJECT_PREFIX=alpha
- export NO_REPLY_MAIL_ADDRESS=no-reply@erasme.org
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
......
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
[travis-url]: https://travis-ci.org/nestjs/nest
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a>
<a href="https://travis-ci.org/nestjs/nest"><img src="https://api.travis-ci.org/nestjs/nest.svg?branch=master" alt="Travis" /></a>
<a href="https://travis-ci.org/nestjs/nest"><img src="https://img.shields.io/travis/nestjs/nest/master.svg?label=linux" alt="Linux" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#5" alt="Coverage" /></a>
<a href="https://gitter.im/nestjs/nestjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://badges.gitter.im/nestjs/nestjs.svg" alt="Gitter" /></a>
<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a>
<a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
## Environment variables
In order to run the code, some environment variables are needed. They are specified in the `template.env` file at the root of the project.
For a local deployment:
1. `cp template.env .env`
2. Edit .env according to the chosen configuration
The values will be read from the file by default, but you can override any of those by exporting manually the variable before launching the service.
## Running the app without docker
You will need to provide a healthy connection to a database in order for the service to start.
```bash
# development
......@@ -43,15 +26,24 @@ $ npm run start
# watch mode
$ npm run start:dev
# incremental rebuild (webpack)
$ npm run webpack
$ npm run start:hmr
# production mode
$ npm run start:prod
```
## Test
## Running the app with docker
```bash
# build
$ docker-compose build
# deploy
$ docker-compose up [-d]
# build and deploy
$ docker-compose up --build [-d]
```
<!-- ## Test
```bash
# unit tests
......@@ -62,18 +54,6 @@ $ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
``` -->
## License
Nest is [MIT licensed](LICENSE).
......@@ -8,27 +8,13 @@ services:
ports:
- ${MAIL_SERVICE_BIND_PORT}:3000
environment:
- USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX}
- RABBITMQ_USER=rabbitmq-user
- RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}
- GROUP_HEADER=x-consumer-groups
- EMAIL_WRITER_GROUP_NAME=email-writer
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- MAIL_SUBJECT_PREFIX=${MAIL_SUBJECT_PREFIX}
- USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX}
- NO_REPLY_MAIL_ADDRESS=${NO_REPLY_MAIL_ADDRESS}
- ADDITIONAL_FEEDBACK_EMAILS=${ADDITIONAL_FEEDBACK_EMAILS}
restart: unless-stopped
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- ${RABBITMQ_LISTENING_PORT}:5672 # standard port for communication
- ${RABBITMQ_GUI_PORT}:15672 # graphic interface
environment:
- RABBITMQ_DEFAULT_USER=rabbitmq-user
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}
volumes:
- rabbitmq-data-volume:/var/lib/rabbitmq
restart: unless-stopped
volumes:
rabbitmq-data-volume:
- GROUP_HEADER=x-consumer-groups
- EMAIL_WRITER_GROUP_NAME=email-writer
- IMAGE_HOST=https://minio.alpha.grandlyon.com/email-template-assets
restart: unless-stopped
\ No newline at end of file
favicon.ico

4.19 KB | W: | H:

favicon.ico

31.3 KB | W: | H:

favicon.ico
favicon.ico
favicon.ico
favicon.ico
  • 2-up
  • Swipe
  • Onion skin
{
"name": "service-email",
"version": "1.2.0",
"version": "1.2.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -7787,6 +7787,11 @@
"which": "^1.3.0"
}
},
"nodemailer": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.2.1.tgz",
"integrity": "sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g=="
},
"nodemon": {
"version": "1.18.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.9.tgz",
......
{
"name": "service-email",
"version": "1.2.0",
"version": "1.2.1",
"description": "description",
"author": "",
"license": "MIT",
......@@ -25,11 +25,11 @@
"@nestjs/core": "^5.1.0",
"@nestjs/swagger": "^2.5.1",
"@nestjs/terminus": "^5.5.0",
"amqplib": "^0.5.2",
"class-transformer": "^0.1.9",
"class-validator": "^0.9.1",
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"nodemailer": "^6.2.1",
"reflect-metadata": "^0.1.12",
"rxjs": "^6.2.2",
"serve-favicon": "^2.5.0",
......@@ -72,4 +72,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}
\ No newline at end of file
import { Logger } from '@nestjs/common';
export class AppLogger extends Logger {
log(message: string, context?: string) {
// add your tailored logic here
super.log(`[log] ${message}`, context);
}
warn(message: string, context?: string) {
// add your tailored logic here
super.warn(`[warn] ${message}`, context);
}
error(message: string, trace?: string, context?: string) {
// add your tailored logic here
super.error(`[error] ${message}`, trace, context);
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import { ConfigModule } from './configuration/config.module';
import { APP_GUARD } from '@nestjs/core';
import { GroupsGuard } from './guards/groups.guards';
import { HealthModule } from './health/health.module';
import { AppLogger } from './app-logger';
@Module({
imports: [
......@@ -13,6 +14,7 @@ import { HealthModule } from './health/health.module';
],
controllers: [],
providers: [
AppLogger,
{
provide: APP_GUARD,
useClass: GroupsGuard,
......
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { config } from './config';
export class ConfigService {
private _config = config;
constructor() {
// Only for development purpose, set environment variable based on the env file provided
if (process.env.NODE_ENV === 'LOCAL') {
const envConfig = dotenv.parse(fs.readFileSync(`./src/configuration/${process.env.NODE_ENV}.env`));
for (const k in envConfig) {
if (envConfig.hasOwnProperty(k)) {
process.env[k] = envConfig[k];
}
}
}
// Initializing conf with values from var env
this._config.rabbitMQ.user = process.env.RABBITMQ_USER;
this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD;
this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX;
this._config.groupNames.emailWriter = process.env.EMAIL_WRITER_GROUP_NAME;
this._config.groupHeader = process.env.GROUP_HEADER;
this._config.smtpConfig.host = process.env.SMTP_HOST;
this._config.smtpConfig.port = process.env.SMTP_PORT;
this._config.mailSubjectPrefix = process.env.MAIL_SUBJECT_PREFIX;
this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX;
this._config.noReplyMailAddress = process.env.NO_REPLY_MAIL_ADDRESS;
this._config.additionalFeedbackEmails = process.env.ADDITIONAL_FEEDBACK_EMAILS;
this._config.groupHeader = process.env.GROUP_HEADER;
this._config.groupNames.emailWriter = process.env.EMAIL_WRITER_GROUP_NAME;
this._config.imageHost = process.env.IMAGE_HOST;
}
get config() {
......
export const config = {
rabbitMQ: {
user: null,
password: null,
host: 'rabbitmq',
port: '5672',
smtpConfig: {
host: '',
port: null,
secure: false,
tls: {
// do not fail on invalid certs
rejectUnauthorized: false,
},
// auth: {
// user: process.env.SMTP_USER,
// pass: process.env.SMTP_PASS
// }
},
mailerQueue: 'portail-data-send-email',
userSupportMailbox: '',
imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets',
imageHost: '',
groupNames: {
emailWriter: '',
},
......
RABBITMQ_USER=
RABBITMQ_PASSWORD=
USER_SUPPORT_MAILBOX=
MAIL_SUBJECT_PREFIX=
NO_REPLY_MAIL_ADDRESS=
ADDITIONAL_FEEDBACK_EMAILS=
GROUP_HEADER=
EMAIL_WRITER_GROUP_NAME=
\ No newline at end of file
import { Controller, Post, Body, InternalServerErrorException, Logger, HttpCode, Req } from '@nestjs/common';
import { Controller, Post, Body, InternalServerErrorException, HttpCode, Req } from '@nestjs/common';
import { ContactForm, EmailWithoutFrom, FeedbackForm } from './email';
import { EmailService } from './email.service';
import { ApiBadRequestResponse, ApiOkResponse, ApiUseTags, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger';
import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger';
import { Groups } from '../decorators/groups.decorators';
import { handleError } from '../helpers';
@Controller()
export class EmailController {
constructor(
private emailService: EmailService,
) {}
) { }
@Post('contact')
// tslint:disable-next-line:max-line-length
@ApiOperation({ title: 'Send email to admin (emails defined as var env of the project, see docker-compose.yml file) and recap email to user email.' })
@ApiOkResponse({ description: 'OK' })
@ApiBadRequestResponse({ description: 'Missing fields' })
@ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'})
@ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' })
@HttpCode(200)
async create(@Body() contactForm: ContactForm) {
try {
return await this.emailService.sendContactEmails(contactForm);
} catch (error) {
Logger.log(error);
throw new InternalServerErrorException();
handleError(error, new InternalServerErrorException());
}
}
......@@ -32,14 +32,13 @@ export class EmailController {
@ApiOperation({ title: 'Send email to admin with the user feedback' })
@ApiOkResponse({ description: 'OK' })
@ApiBadRequestResponse({ description: 'Missing fields' })
@ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'})
@ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' })
@HttpCode(200)
async sendFeedback(@Body() feedbackForm: FeedbackForm, @Req() req) {
try {
return await this.emailService.sendFeedback(feedbackForm, req.headers['user-agent']);
} catch (error) {
Logger.log(error);
throw new InternalServerErrorException();
handleError(error, new InternalServerErrorException());
}
}
......@@ -48,14 +47,13 @@ export class EmailController {
@ApiOperation({ title: 'Send email.' })
@ApiOkResponse({ description: 'OK' })
@ApiBadRequestResponse({ description: 'Missing fields' })
@ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'})
@ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' })
@HttpCode(200)
async createEmail(@Body() email: EmailWithoutFrom) {
try {
return await this.emailService.send(email);
} catch (error) {
Logger.log(error);
throw new InternalServerErrorException();
handleError(error, new InternalServerErrorException());
}
}
}
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import * as amqp from 'amqplib';
import { ContactForm, Email, EmailWithoutFrom, FeedbackForm } from './email';
import { ConfigService } from '../configuration/config.service';
import { buildContactAdminEmail, buildContactUserEmail } from '../email-templates/contact';
import { buildFeedbackEmail } from '../email-templates/feedback';
import * as useragent from 'useragent';
import * as Nodemailer from 'nodemailer';
import moment = require('moment-timezone');
moment.tz.setDefault('Europe/Paris');
@Injectable()
export class EmailService {
config: any = {};
private logger: Logger;
private config;
constructor(private configService: ConfigService) {
this.logger = new Logger(EmailService.name);
this.config = this.configService.config;
}
async sendContactEmails(contactForm: ContactForm) {
Logger.log('[-] sendContactEmails method');
this.logger.log('Entering function', `${EmailService.name} - ${this.sendContactEmails.name}`);
const adminEmailBody = buildContactAdminEmail({
subject: contactForm.subject,
......@@ -55,7 +58,7 @@ export class EmailService {
}
async sendFeedback(feedbackForm: FeedbackForm, userAgent) {
Logger.log('[-] sendFeedback method');
this.logger.log('Entering function', `${EmailService.name} - ${this.sendFeedback.name}`);
const userAgentParsed = useragent.parse(userAgent);
let userAgentString = '';
......@@ -90,10 +93,7 @@ export class EmailService {
}
async send(emailInfo: EmailWithoutFrom) {
let conn, ch;
// tslint:disable-next-line:max-line-length
const rabbitmqUrl = `amqp://${this.config.rabbitMQ.user}:${this.config.rabbitMQ.password}@${this.config.rabbitMQ.host}:${this.config.rabbitMQ.port}`;
const mailerQueue = this.config.mailerQueue;
this.logger.log('Entering function', `${EmailService.name} - ${this.send.name}`);
let email = new Email();
email.from = this.config.noReplyMailAddress;
......@@ -103,45 +103,23 @@ export class EmailService {
email.subject = `[${this.config.mailSubjectPrefix}] ${email.subject}`;
}
Logger.log('[-] send method');
Logger.log(email);
// Connect to rabbitmq
try {
conn = await amqp.connect(rabbitmqUrl);
} catch (error) {
Logger.error(' [x] Error connecting to RabbitMQ: ', JSON.stringify(error));
throw new InternalServerErrorException('Could not connect to rabbitMQ.');
}
try {
// Create a communication channel
ch = await conn.createChannel();
} catch (error) {
Logger.error(' [x] Error creating channel: ', JSON.stringify(error));
throw new InternalServerErrorException('Could not create channel.');
}
// Stringify and bufferise message
const buffer = Buffer.from(JSON.stringify(email));
const transporter = Nodemailer.createTransport(this.config.smtpConfig);
try {
await ch.assertQueue(mailerQueue, { durable: true });
} catch (error) {
Logger.error(' [x] Error creating channel: ', JSON.stringify(error));
throw new InternalServerErrorException('Could not assert channel.');
}
await transporter.verify().catch((error) => {
this.logger.error('SMTP connection failed.', error, `${EmailService.name} - ${this.send.name}`);
throw new InternalServerErrorException({ error, message: 'SMTP connection failed.' });
});
try {
await ch.sendToQueue(mailerQueue, buffer, { persistent: true });
} catch (error) {
Logger.error(' [x] Error sending to queue: ', JSON.stringify(error));
throw new InternalServerErrorException('Could not send to queue.');
}
this.logger.log('SMTP Server is ready to receive messages', `${EmailService.name} - ${this.send.name}`);
Logger.log(`Sent to queue ${mailerQueue}: ${JSON.stringify(email)}`);
await transporter.sendMail(email).catch((error) => {
this.logger.error('Couldn\'t send email.', error, `${EmailService.name} - ${this.send.name}`);
transporter.close();
throw new InternalServerErrorException({ error, message: 'Couldn\'t send email.' });
});
setTimeout(() => { conn.close(); }, 500);
transporter.close();
this.logger.log('Email sent to SMTP server', `${EmailService.name} - ${this.send.name}`);
return;
}
......
......@@ -4,10 +4,19 @@ import { ConfigService } from '../configuration/config.service';
@Injectable()
export class GroupsGuard implements CanActivate {
constructor(private readonly reflector: Reflector, private configService: ConfigService) {}
private logger: Logger;
constructor(
private readonly reflector: Reflector,
private configService: ConfigService,
) {
this.logger = new Logger(GroupsGuard.name);
}
canActivate(context: ExecutionContext): boolean {
Logger.log('[+] GroupMiddleware');
this.logger.log('Entering function', `${GroupsGuard.name} - ${this.canActivate.name}`);
// We get the groups specified on the endpoint with the @Groups annotation
// We need then to get the real group names associated to those keys from the config file
let groups = this.reflector.get<string[]>('groups', context.getHandler());
......@@ -31,13 +40,13 @@ export class GroupsGuard implements CanActivate {
const groupHeader = request.headers[this.configService.config.groupHeader];
if (!groupHeader) {
Logger.log(' [-] no group header');
this.logger.log('No group header');
return false;
}
const consumerGroups = groupHeader.split(',').map(e => e.trim());
Logger.log(` [-] consumer groups: ${consumerGroups}`);
this.logger.log(`Consumer groups: ${consumerGroups}`);
const hasGroup = () => consumerGroups.some((group) => groups.includes(group));
......
......@@ -2,33 +2,21 @@ import {
TerminusEndpoint,
TerminusOptionsFactory,
TerminusModuleOptions,
MicroserviceHealthIndicator,
} from '@nestjs/terminus';
import { Injectable } from '@nestjs/common';
import { Transport } from '@nestjs/microservices';
import { ConfigService } from '../configuration/config.service';
import { SmtpHealthIndicator } from './smtp.healthIndicator';
@Injectable()
export class HealthCheckService implements TerminusOptionsFactory {
constructor(
private configService: ConfigService,
private readonly _microserviceHealthIndicator: MicroserviceHealthIndicator,
private readonly _smtpHealthIndicator: SmtpHealthIndicator,
) { }
createTerminusOptions(): TerminusModuleOptions {
const config = this.configService.config;
const healthEndpoint: TerminusEndpoint = {
url: '/health',
healthIndicators: [
async () => this._microserviceHealthIndicator.pingCheck('rabbitMQ', {
transport: Transport.RMQ,
options: {
urls: [
`amqp://${config.rabbitMQ.user}:${config.rabbitMQ.password}@${config.rabbitMQ.host}:${config.rabbitMQ.port}`,
],
queue: config.mailerQueue,
},
}),
async () => this._smtpHealthIndicator.checkSMTPConnection(),
],