Skip to content

Commit

Permalink
system event handling
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 15, 2024
1 parent f810ca0 commit 65b5fb5
Show file tree
Hide file tree
Showing 15 changed files with 134 additions and 8 deletions.
3 changes: 2 additions & 1 deletion api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { AuthModule } from '@api/modules/auth/auth.module';
import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard';
import { RolesGuard } from '@api/modules/auth/guards/roles.guard';
import { NotificationsModule } from '@api/modules/notifications/notifications.module';
import { EventsModule } from '@api/modules/events/events.module';

@Module({
imports: [ApiConfigModule, AuthModule, NotificationsModule],
imports: [ApiConfigModule, AuthModule, NotificationsModule, EventsModule],
controllers: [AppController],
providers: [
AppService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { User } from '@shared/entities/users/user.entity';
import * as bcrypt from 'bcrypt';
import { LoginDto } from '@api/modules/auth/dtos/login.dto';
import { JwtPayload } from '@api/modules/auth/strategies/jwt.strategy';
import { EventBus } from '@nestjs/cqrs';
import { UserSignedUpEvent } from '@api/modules/events/user-events/user-signed-up.event';

@Injectable()
export class AuthenticationService {
constructor(
private readonly usersService: UsersService,
private readonly jwt: JwtService,
private readonly eventBus: EventBus,
) {}
async validateUser(email: string, password: string): Promise<User> {
const user = await this.usersService.findByEmail(email);
Expand All @@ -22,10 +25,11 @@ export class AuthenticationService {

async signUp(signupDto: LoginDto): Promise<void> {
const passwordHash = await bcrypt.hash(signupDto.password, 10);
await this.usersService.createUser({
const newUser = await this.usersService.createUser({
email: signupDto.email,
password: passwordHash,
});
this.eventBus.publish(new UserSignedUpEvent(newUser.id, newUser.email));
}

async logIn(user: User): Promise<{ user: User; accessToken: string }> {
Expand Down
5 changes: 5 additions & 0 deletions api/src/modules/auth/services/password-recovery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { UsersService } from '@api/modules/users/users.service';
import { JwtService } from '@nestjs/jwt';
import { AuthMailer } from '@api/modules/auth/services/auth.mailer';
import { EventBus } from '@nestjs/cqrs';
import { PasswordRecoveryRequestedEvent } from '@api/modules/events/user-events/password-recovery-requested.event';

@Injectable()
export class PasswordRecoveryService {
Expand All @@ -10,6 +12,7 @@ export class PasswordRecoveryService {
private readonly users: UsersService,
private readonly jwt: JwtService,
private readonly authMailer: AuthMailer,
private readonly eventBus: EventBus,
) {}

async recoverPassword(email: string, origin: string): Promise<void> {
Expand All @@ -18,6 +21,7 @@ export class PasswordRecoveryService {
this.logger.warn(
`Email ${email} not found when trying to recover password`,
);
this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, null));
return;
}
const token = this.jwt.sign({ id: user.id });
Expand All @@ -26,5 +30,6 @@ export class PasswordRecoveryService {
token,
origin,
});
this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, user.id));
}
}
5 changes: 3 additions & 2 deletions api/src/modules/config/app-config.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DATABASE_ENTITIES } from '@shared/entities/database.entities';
import { COMMON_DATABASE_ENTITIES } from '@shared/entities/database.entities';
import { ApiEventsEntity } from '@api/modules/events/api-events/api-events.entity';

export type JWTConfig = {
secret: string;
Expand All @@ -24,7 +25,7 @@ export class ApiConfigService {
username: this.configService.get('DB_USERNAME'),
password: this.configService.get('DB_PASSWORD'),
database: this.configService.get('DB_NAME'),
entities: DATABASE_ENTITIES,
entities: [...COMMON_DATABASE_ENTITIES, ApiEventsEntity],
synchronize: true,
ssl: this.isProduction()
? { require: true, rejectUnauthorized: false }
Expand Down
2 changes: 2 additions & 0 deletions api/src/modules/events/api-events/api-events.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export class ApiEventsEntity {
@Column({
type: 'enum',
enum: API_EVENT_TYPES,
name: 'event_type',
enumName: 'api_event_types',
})
eventType: API_EVENT_TYPES;

Expand Down
7 changes: 7 additions & 0 deletions api/src/modules/events/api-events/email-failed.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// src/events/notification-events/email-failed.event.ts
export class EmailFailedEvent {
constructor(
public readonly email: string,
public readonly errorMessage: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// src/events/notification-events/handlers/email-failed-event.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { EmailFailedEvent } from '../email-failed.event';
import { API_EVENT_TYPES } from '@api/modules/events/events.enum';
import { ApiEventsService } from '@api/modules/events/api-events/api-events.service';

@EventsHandler(EmailFailedEvent)
export class EmailFailedEventHandler
implements IEventHandler<EmailFailedEvent>
{
constructor(private readonly apiEventsService: ApiEventsService) {}

async handle(event: EmailFailedEvent): Promise<void> {
console.warn('EmailFailedEventHandler', event);
await this.apiEventsService.create({
eventType: API_EVENT_TYPES.EMAIL_FAILED,
resourceId: null,
payload: {
email: event.email,
errorMessage: event.errorMessage,
},
});
}
}
2 changes: 2 additions & 0 deletions api/src/modules/events/events.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export enum API_EVENT_TYPES {
USER_SIGNED_UP = 'user.signed_up',
USER_PASSWORD_RECOVERY_REQUESTED = 'user.password_recovery_requested',
USER_PASSWORD_RECOVERY_REQUESTED_NON_EXISTENT = 'user.password_recovery_requested_non_existent',

EMAIL_FAILED = 'system.email.failed',
// More events to come....
}

Expand Down
15 changes: 13 additions & 2 deletions api/src/modules/events/events.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { ApiEventsModule } from './api-events/api-events.module';
import { CqrsModule } from '@nestjs/cqrs';
import { UserSignedUpEventHandler } from '@api/modules/events/user-events/handlers/user-signed-up.handler';
import { PasswordRecoveryRequestedEventHandler } from '@api/modules/events/user-events/handlers/password-recovery-requested.handler';
import { EmailFailedEventHandler } from '@api/modules/events/api-events/handlers/emai-failed-event.handler';

@Global()
@Module({
imports: [ApiEventsModule],
imports: [CqrsModule, ApiEventsModule],
providers: [
UserSignedUpEventHandler,
PasswordRecoveryRequestedEventHandler,
EmailFailedEventHandler,
],
exports: [CqrsModule],
})
export class EventsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// src/events/user-events/handlers/password-recovery-requested.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { PasswordRecoveryRequestedEvent } from '../password-recovery-requested.event';
import { ApiEventsService } from '@api/modules/events/api-events/api-events.service';
import { API_EVENT_TYPES } from '@api/modules/events/events.enum';

@EventsHandler(PasswordRecoveryRequestedEvent)
export class PasswordRecoveryRequestedEventHandler
implements IEventHandler<PasswordRecoveryRequestedEvent>
{
constructor(private readonly apiEventsService: ApiEventsService) {}

async handle(event: PasswordRecoveryRequestedEvent): Promise<void> {
console.warn('PasswordRecoveryRequestedEventHandler', event);
await this.apiEventsService.create({
eventType: API_EVENT_TYPES.USER_PASSWORD_RECOVERY_REQUESTED,
resourceId: event.userId,
payload: {
email: event.email,
warning: event.userId
? null
: `Email ${event.email} not found when trying to recover password`,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// src/events/user-events/handlers/user-signed-up.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { UserSignedUpEvent } from '../user-signed-up.event';
import { ApiEventsService } from '@api/modules/events/api-events/api-events.service';
import { API_EVENT_TYPES } from '@api/modules/events/events.enum';

@EventsHandler(UserSignedUpEvent)
export class UserSignedUpEventHandler
implements IEventHandler<UserSignedUpEvent>
{
constructor(private readonly apiEventsService: ApiEventsService) {}

async handle(event: UserSignedUpEvent): Promise<void> {
await this.apiEventsService.create({
eventType: API_EVENT_TYPES.USER_SIGNED_UP,
resourceId: event.userId,
payload: {
email: event.email,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// src/events/user-events/password-recovery-requested.event.ts
export class PasswordRecoveryRequestedEvent {
constructor(
public readonly email: string,
public readonly userId?: string,
) {}
}
7 changes: 7 additions & 0 deletions api/src/modules/events/user-events/user-signed-up.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// src/events/user-events/user-signed-up.event.ts
export class UserSignedUpEvent {
constructor(
public readonly userId: string,
public readonly email: string,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ import {
SendMailDTO,
} from '@api/modules/notifications/email/email-service.interface';
import { ConfigService } from '@nestjs/config';
import { EventBus } from '@nestjs/cqrs';
import { EmailFailedEventHandler } from '@api/modules/events/api-events/handlers/emai-failed-event.handler';
import { EmailFailedEvent } from '@api/modules/events/api-events/email-failed.event';

@Injectable()
export class NodemailerEmailService implements IEmailServiceInterface {
logger: Logger = new Logger(NodemailerEmailService.name);
private transporter: nodemailer.Transporter;
private readonly domain: string;

constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
private readonly eventBus: EventBus,
) {
const { accessKeyId, secretAccessKey, region, domain } =
this.getMailConfig();
const ses = new aws.SESClient({
Expand All @@ -40,6 +46,7 @@ export class NodemailerEmailService implements IEmailServiceInterface {
});
} catch (e) {
this.logger.error(`Error sending email: ${JSON.stringify(e)}`);
this.eventBus.publish(new EmailFailedEvent(sendMailDTO.to, e.message));
throw new ServiceUnavailableException('Could not send email');
}
}
Expand Down
2 changes: 1 addition & 1 deletion shared/entities/database.entities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { User } from "@shared/entities/users/user.entity";

export const DATABASE_ENTITIES = [User];
export const COMMON_DATABASE_ENTITIES = [User];

0 comments on commit 65b5fb5

Please sign in to comment.