Skip to content

Commit

Permalink
feat: add mailer
Browse files Browse the repository at this point in the history
  • Loading branch information
josefrnandezz committed Sep 28, 2022
1 parent a77db31 commit 6c3b3d7
Show file tree
Hide file tree
Showing 26 changed files with 1,907 additions and 57 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
import { ArtistModule } from '@melomaniapp/nestjs/artist';
import { AuthModule } from '@melomaniapp/nestjs/auth';
import { EstablishmentModule } from '@melomaniapp/nestjs/establishment';
import { GenreModule } from '@melomaniapp/nestjs/genre';
import { EventModule } from '@melomaniapp/nestjs/event';
import { FollowModule } from '@melomaniapp/nestjs/follow';
import { GenreModule } from '@melomaniapp/nestjs/genre';
import { MailModule } from '@melomaniapp/nestjs/mailer';
import { UserModule } from '@melomaniapp/nestjs/user';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
Expand All @@ -27,6 +28,7 @@ import { appProviders } from './app.providers';
isGlobal: true,
load: [configuration],
}),
MailModule,
ConsoleModule,
CqrsModule,
MongooseModule.forRoot(
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/assets/templates/cancelado.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<p>Hola,</p>
<p>Este evento se ha cancelado:</p>
<p>
{{ name }}
</p>

7 changes: 7 additions & 0 deletions apps/api/src/assets/templates/confirmation.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Hey {{ name }},</p>
<p>Please click below to confirm your email</p>
<p>
<a href="{{ url }}">Confirm</a>
</p>

<p>If you did not request this email you can safely ignore it.</p>
9 changes: 8 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.2'

services:
eventstore.db:
image: ghcr.io/eventstore/eventstore@sha256:ab30bf2a2629e2c8710f8f7fdcb74df5466c6b3b2200c8e9ad8a797ed138012a
image: eventstore/eventstore:20.10.2-buster-slim
environment:
- EVENTSTORE_CLUSTER_SIZE=1
- EVENTSTORE_RUN_PROJECTIONS=All
Expand Down Expand Up @@ -37,6 +37,13 @@ services:
ports:
- 8081:8081


mailcatcher:
image: dockage/mailcatcher
ports:
- 1080:1080
- 1025:1025

volumes:
eventstore-volume-data:
eventstore-volume-logs:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EventDTO } from '@melomaniapp/contracts/event';
import { MailService } from '@melomaniapp/nestjs/mailer';
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
Expand All @@ -11,10 +13,14 @@ export class EventWasCancelledProjection
{
constructor(
@InjectModel(EVENTS_PROJECTION)
private readonly events: Model<EventDocument>
private readonly events: Model<EventDocument>,
private readonly mailService: MailService
) {}

async handle(event: EventWasCancelled) {
await this.events.findByIdAndDelete(event.aggregateId).exec();
const eventDto = await this.events
.findByIdAndDelete(event.aggregateId)
.lean<EventDTO>();
await this.mailService.sendEventWasCanceled(['[email protected]'], eventDto);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { EventWasCreated } from '../../../domain';
import { EventDocument, EVENTS_PROJECTION } from './events.schema';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { catchError } from 'rxjs';
import { v4 } from 'uuid';
import { FollowGuard } from '../auth';
import { FollowService } from '../services';
import { MailService } from '@melomaniapp/nestjs/mailer';

@Controller('follows')
export class FollowController {
Expand Down
3 changes: 3 additions & 0 deletions libs/nestjs/mailer/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
}
18 changes: 18 additions & 0 deletions libs/nestjs/mailer/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
7 changes: 7 additions & 0 deletions libs/nestjs/mailer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# nestjs-mailer

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test nestjs-mailer` to execute the unit tests via [Jest](https://jestjs.io).
15 changes: 15 additions & 0 deletions libs/nestjs/mailer/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
displayName: 'nestjs-mailer',
preset: '../../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node',
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../../coverage/libs/nestjs/mailer',
};
2 changes: 2 additions & 0 deletions libs/nestjs/mailer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/mailer.module';
export * from './lib/mailer.service';
28 changes: 28 additions & 0 deletions libs/nestjs/mailer/src/lib/mailer.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Global, Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';

import { mailProvider, MailService } from './mailer.service';

@Global()
@Module({
imports: [
MailerModule.forRoot({
transport: 'smtp://localhost:1025',
defaults: {
from: '"No Reply" <[email protected]>',
},
template: {
dir: join(__dirname, 'assets/templates'),
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
options: {
strict: true,
},
},
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}
44 changes: 44 additions & 0 deletions libs/nestjs/mailer/src/lib/mailer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { UserDto } from '@melomaniapp/contracts/user';
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
import { EventDTO } from '@melomaniapp/contracts/event';

export const MAIL_SERVICE = 'MAIL_SERVICE';

@Injectable()
export class MailService {
constructor(private mailerService: MailerService) {}

async sendUserConfirmation(user: UserDto, token: string) {
const url = `example.com/auth/confirm?token=${token}`;

await this.mailerService.sendMail({
to: user.email,
// from: '"Support Team" <[email protected]>', // override default from
subject: 'Welcome to Nice App! Confirm your Email',
template: './confirmation', // `.hbs` extension is appended automatically
context: {
// ✏️ filling curly brackets with content
name: user.username,
url,
},
});
}

async sendEventWasCanceled(emails: string[], event: EventDTO) {
await this.mailerService.sendMail({
to: emails,
// from: '"Support Team" <[email protected]>', // override default from
subject: 'Evento cancelado',
template: './cancelado', // `.hbs` extension is appended automatically
context: {
name: event.name,
},
});
}
}

export const mailProvider = {
provide: MAIL_SERVICE,
useClass: MailService,
};
13 changes: 13 additions & 0 deletions libs/nestjs/mailer/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
12 changes: 12 additions & 0 deletions libs/nestjs/mailer/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"],
"target": "es6"
},
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
"include": ["**/*.ts"]
}
19 changes: 19 additions & 0 deletions libs/nestjs/mailer/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}
2 changes: 1 addition & 1 deletion libs/nestjs/user/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export * from './domain/model/user-id';
export * from './infrastructure/user.module';
export * from './application/services/user-finder.interface';
export * from './infrastructure/services/user-finder.service';
export * from './/infrastructure/read-model';
export * from './infrastructure/read-model';
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {
IdAlreadyRegisteredError,
IdNotFoundError,
} from '@aulasoftwarelibre/nestjs-eventstore';
import { EstablishmentDTO } from '@melomaniapp/contracts/establishment';
import { ArtistDTO } from '@melomaniapp/contracts/artist';
import { EstablishmentDTO } from '@melomaniapp/contracts/establishment';
import {
CreateUserDto,
EditUserDto,
UserDto,
} from '@melomaniapp/contracts/user';
import { catchError, Role, Roles, User } from '@melomaniapp/nestjs/common';
import { MailService } from '@melomaniapp/nestjs/mailer';
import {
Body,
ConflictException,
Expand All @@ -27,14 +28,17 @@ import {
import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Response } from 'express';
import { ACGuard } from 'nest-access-control';
import { UserGuard } from '../auth';

import { UserGuard } from '../auth';
import { UserService } from '../services';

@ApiBearerAuth()
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
constructor(
private readonly userService: UserService,
private readonly mailService: MailService
) {}

@Post()
@Roles(Role.Admin)
Expand Down Expand Up @@ -100,6 +104,7 @@ export class UserController {
@UseGuards(UserGuard, ACGuard)
async getMyUser(@User() user: UserDto): Promise<UserDto> {
try {
await this.mailService.sendUserConfirmation(user, 'token');
return this.userService.findOneById(user._id);
} catch (error) {
throw catchError(error);
Expand Down
2 changes: 2 additions & 0 deletions libs/nestjs/user/src/infrastructure/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventStoreModule } from '@aulasoftwarelibre/nestjs-eventstore';
import { MailModule } from '@melomaniapp/nestjs/mailer';
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { MongooseModule } from '@nestjs/mongoose';
Expand All @@ -15,6 +16,7 @@ import { userProviders } from './user.providers';
controllers: [UserController],
imports: [
CqrsModule,
MailModule,
EventStoreModule.forFeature([User], eventTransformers),
MongooseModule.forFeature([
{
Expand Down
8 changes: 4 additions & 4 deletions libs/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export * from './lib/components/AccountMenu/AccountMenu';
export * from './lib/components/CityDropdown/CityDropdown';
export * from './lib/components/Establishment/EstablishmentForm/EstablishmentForm';
export * from './lib/components/FollowButton/FollowButton';
export * from './lib/components/Genre/GenreFilter/GenreFilter';
export * from './lib/components/Genre/GenreItem/GenreItem';
export * from './lib/components/Genre/GenreList/GenreList';
export * from './lib/components/IconText/IconText';
export * from './lib/components/ProfileHeader/ProfileHeader';
export * from './lib/components/ProfileSwitcher/ProfileSwitcher';
export * from './lib/components/FollowButton/FollowButton';
export * from './lib/components/SignIn/SignIn';
export * from './lib/components/CityDropdown/CityDropdown';
export * from './lib/components/IconText/IconText';
export * from './lib/utils';
export * from './lib/components/ProfileHeader/ProfileHeader';
41 changes: 41 additions & 0 deletions libs/ui/src/lib/components/CityDropdown/CityDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Select, Spin } from 'antd';
import React from 'react';
import { useCities } from '@melomaniapp/hooks';

const { Option } = Select;

export interface CityDropdownProps {
selectedCity?: string;
onChangeHandler?: (value: string) => void;
}

export const CityDropdown: React.FC<CityDropdownProps> = ({
selectedCity,
onChangeHandler,
}) => {
const { data: cities, isLoading } = useCities();

if (isLoading) {
<div style={{ display: 'flex', alignItems: 'center' }}>
<Spin size="large" style={{ margin: 'auto' }} />;
</div>;
}

return (
<Select
placeholder="Ciudad"
showSearch={false}
defaultValue={selectedCity}
showArrow
onChange={onChangeHandler}
>
{cities?.map((city) => (
<Option key={city} value={city}>
{city}
</Option>
))}
</Select>
);
};

export default CityDropdown;
Loading

0 comments on commit 6c3b3d7

Please sign in to comment.