Commit e1e9fa9d authored by FORESTIER Fabien's avatar FORESTIER Fabien
Browse files

Update log system, simplify var env export for local deploy, remove heavy...

Update log system, simplify var env export for local deploy, remove heavy email bodies from the logs
parent 71c198db
Pipeline #2449 passed with stages
in 3 minutes and 28 seconds
<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,14 +8,18 @@ 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
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_LISTENING_PORT=5672
- MAILER_QUEUE=portail-data-send-email
- 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}
- GROUP_HEADER=x-consumer-groups
- EMAIL_WRITER_GROUP_NAME=email-writer
- IMAGE_HOST=https://minio.alpha.grandlyon.com/email-template-assets
restart: unless-stopped
rabbitmq:
......
{
"name": "service-email",
"version": "1.2.0",
"version": "1.2.1",
"description": "description",
"author": "",
"license": "MIT",
......@@ -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.rabbitMQ.host = process.env.RABBITMQ_HOST;
this._config.rabbitMQ.port = process.env.RABBITMQ_LISTENING_PORT;
this._config.mailerQueue = process.env.MAILER_QUEUE;
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() {
......
......@@ -2,12 +2,12 @@ export const config = {
rabbitMQ: {
user: null,
password: null,
host: 'rabbitmq',
port: '5672',
host: null,
port: null,
},
mailerQueue: 'portail-data-send-email',
mailerQueue: '',
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';
@Controller()
......@@ -9,20 +9,19 @@ 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, this is probably a rabbitMQ related error (unreachable service...)' })
@HttpCode(200)
async create(@Body() contactForm: ContactForm) {
try {
return await this.emailService.sendContactEmails(contactForm);
} catch (error) {
Logger.log(error);
throw new InternalServerErrorException();
}
}
......@@ -32,13 +31,12 @@ 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, this is probably a rabbitMQ related error (unreachable service...)' })
@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();
}
}
......@@ -48,13 +46,12 @@ 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, this is probably a rabbitMQ related error (unreachable service...)' })
@HttpCode(200)
async createEmail(@Body() email: EmailWithoutFrom) {
try {
return await this.emailService.send(email);
} catch (error) {
Logger.log(error);
throw new InternalServerErrorException();
}
}
......
......@@ -10,14 +10,17 @@ moment.tz.setDefault('Europe/Paris');
@Injectable()
export class EmailService {
private logger: Logger;
config: any = {};
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,6 +93,8 @@ export class EmailService {
}
async send(emailInfo: EmailWithoutFrom) {
this.logger.log('Entering function', `${EmailService.name} - ${this.send.name}`);
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}`;
......@@ -103,14 +108,11 @@ 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));
Logger.error('Error connecting to RabbitMQ', error, `${EmailService.name} - ${this.send.name}`);
throw new InternalServerErrorException('Could not connect to rabbitMQ.');
}
......@@ -118,7 +120,7 @@ export class EmailService {
// Create a communication channel
ch = await conn.createChannel();
} catch (error) {
Logger.error(' [x] Error creating channel: ', JSON.stringify(error));
Logger.error('Error creating channel', error, `${EmailService.name} - ${this.send.name}`);
throw new InternalServerErrorException('Could not create channel.');
}
......@@ -128,21 +130,22 @@ export class EmailService {
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.');
Logger.error('Error asserting queue', error, `${EmailService.name} - ${this.send.name}`);
throw new InternalServerErrorException('Could not assert queue.');
}
try {
await ch.sendToQueue(mailerQueue, buffer, { persistent: true });
} catch (error) {
Logger.error(' [x] Error sending to queue: ', JSON.stringify(error));
Logger.error('Error sending to queue', error, `${EmailService.name} - ${this.send.name}`);
throw new InternalServerErrorException('Could not send to queue.');
}
Logger.log(`Sent to queue ${mailerQueue}: ${JSON.stringify(email)}`);
Logger.log(
`Sent to queue ${mailerQueue},{ from: ${email.from}, to: ${email.to}, subject: ${email.subject}}`, `${EmailService.name} - ${this.send.name}`,
);
setTimeout(() => { conn.close(); }, 500);
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));
......
......@@ -5,10 +5,13 @@ import { ValidationPipe } from '@nestjs/common';
import * as swStats from 'swagger-stats';
import * as favicon from 'serve-favicon';
import * as path from 'path';
import { AppLogger } from './app-logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create(AppModule, {
logger: false,
});
app.enableCors({ credentials: true, origin: true });
......@@ -30,6 +33,8 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe());
app.useLogger(app.get(AppLogger));
await app.listen(3000);
}
bootstrap();
TAG=<version of the service to deploy>
RABBITMQ_USER=<rabbitMQ user>
RABBITMQ_PASSWORD=<rabbitMQ password>
RABBITMQ_HOST=<rabbitMQ host>
RABBITMQ_LISTENING_PORT=<rabiitMQ listening port, default 5672>
RABBITMQ_GUI_PORT=<RabbitMQ interface, default 15672>
MAILER_QUEUE=<sueue where to write the emails to be sent>
MAIL_SERVICE_BIND_PORT=<listening port of the service>
MAIL_SUBJECT_PREFIX=<prefix used in email subject>
USER_SUPPORT_MAILBOX=<email address of the support>
NO_REPLY_MAIL_ADDRESS=<no-reply email address>
ADDITIONAL_FEEDBACK_EMAILS=<any other email address that should receive the feedback>
GROUP_HEADER=<header name that contains the user group set by the api gateway>
EMAIL_WRITER_GROUP_NAME=<name of the group allowed to send email>
IMAGE_HOST=<host of the images present in the emails body>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment