feat(auth): add OTP verification for email changes on profile update
Email changes via PATCH /api/v1/auth/profile now require OTP verification instead of updating immediately. A 6-digit code is sent to the new email address and must be confirmed via POST /api/v1/auth/profile/verify-email within 10 minutes. Also fixes pre-existing web valuation test failures (formatPrice output format, removed comparables section, missing QueryClientProvider wrapper). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { EmailChangeRequestedEvent } from '@modules/auth';
|
||||
import { LoggerService } from '@modules/shared';
|
||||
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
|
||||
|
||||
@Injectable()
|
||||
export class EmailChangeRequestedListener {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
@OnEvent('user.email_change_requested', { async: true })
|
||||
async handle(event: EmailChangeRequestedEvent): Promise<void> {
|
||||
this.logger.log(
|
||||
`Handling email change OTP for user ${event.aggregateId}`,
|
||||
'EmailChangeRequestedListener',
|
||||
);
|
||||
|
||||
await this.commandBus.execute(
|
||||
new SendNotificationCommand(
|
||||
event.aggregateId,
|
||||
'EMAIL',
|
||||
'user.email_change_otp',
|
||||
{ otpCode: event.otpCode },
|
||||
event.newEmail,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,10 @@ describe('TemplateService', () => {
|
||||
expect(result.body).toContain('/listings/2');
|
||||
});
|
||||
|
||||
it('getTemplateKeys returns all 11 template keys', () => {
|
||||
it('getTemplateKeys returns all 12 template keys', () => {
|
||||
const keys = service.getTemplateKeys();
|
||||
|
||||
expect(keys).toHaveLength(11);
|
||||
expect(keys).toHaveLength(12);
|
||||
expect(keys).toContain('user.registered');
|
||||
expect(keys).toContain('agent.verified');
|
||||
expect(keys).toContain('listing.approved');
|
||||
@@ -96,5 +96,6 @@ describe('TemplateService', () => {
|
||||
expect(keys).toContain('subscription.expiring');
|
||||
expect(keys).toContain('saved_search_alert');
|
||||
expect(keys).toContain('saved_search_digest');
|
||||
expect(keys).toContain('user.email_change_otp');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +75,15 @@ const TEMPLATES: Record<string, TemplateDefinition> = {
|
||||
body: `<h1>Gói đăng ký đã bị huỷ</h1>
|
||||
<p>Gói <strong>{{planTier}}</strong> của bạn đã bị huỷ.</p>
|
||||
<p>Bạn có thể đăng ký lại bất cứ lúc nào để tiếp tục sử dụng đầy đủ tính năng.</p>
|
||||
<p>Trân trọng,<br/>Đội ngũ GoodGo</p>`,
|
||||
},
|
||||
'user.email_change_otp': {
|
||||
subject: 'Xác nhận thay đổi email — GoodGo',
|
||||
body: `<h1>Xác nhận thay đổi email</h1>
|
||||
<p>Bạn đã yêu cầu thay đổi email trên GoodGo. Sử dụng mã OTP sau để xác nhận:</p>
|
||||
<p style="font-size:24px;font-weight:bold;letter-spacing:4px;text-align:center;margin:24px 0;">{{otpCode}}</p>
|
||||
<p>Mã có hiệu lực trong 10 phút.</p>
|
||||
<p>Nếu bạn không yêu cầu, hãy bỏ qua email này.</p>
|
||||
<p>Trân trọng,<br/>Đội ngũ GoodGo</p>`,
|
||||
},
|
||||
'saved_search_alert': {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { AuthModule } from '@modules/auth';
|
||||
import { SendNotificationHandler } from './application/commands/send-notification/send-notification.handler';
|
||||
import { AgentVerifiedListener } from './application/listeners/agent-verified.listener';
|
||||
import { EmailChangeRequestedListener } from './application/listeners/email-change-requested.listener';
|
||||
import { InquiryReceivedListener } from './application/listeners/inquiry-received.listener';
|
||||
import { ListingApprovedListener } from './application/listeners/listing-approved.listener';
|
||||
import { ListingRejectedListener } from './application/listeners/listing-rejected.listener';
|
||||
@@ -21,8 +23,11 @@ import { PrismaNotificationPreferenceRepository } from './infrastructure/reposit
|
||||
import { PrismaNotificationRepository } from './infrastructure/repositories/prisma-notification.repository';
|
||||
import { EmailService } from './infrastructure/services/email.service';
|
||||
import { FcmService } from './infrastructure/services/fcm.service';
|
||||
import { StringeeSmsService } from './infrastructure/services/stringee-sms.service';
|
||||
import { TemplateService } from './infrastructure/services/template.service';
|
||||
import { ZaloOaService } from './infrastructure/services/zalo-oa.service';
|
||||
import { NotificationsController } from './presentation/controllers/notifications.controller';
|
||||
import { NotificationsGateway } from './presentation/gateways/notifications.gateway';
|
||||
|
||||
const CommandHandlers = [SendNotificationHandler];
|
||||
|
||||
@@ -41,10 +46,11 @@ const EventListeners = [
|
||||
InquiryReceivedListener,
|
||||
ListingSoldListener,
|
||||
UserKycUpdatedListener,
|
||||
EmailChangeRequestedListener,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [CqrsModule],
|
||||
imports: [CqrsModule, AuthModule],
|
||||
controllers: [NotificationsController],
|
||||
providers: [
|
||||
// Repositories
|
||||
@@ -54,14 +60,19 @@ const EventListeners = [
|
||||
// Services
|
||||
EmailService,
|
||||
FcmService,
|
||||
StringeeSmsService,
|
||||
ZaloOaService,
|
||||
TemplateService,
|
||||
|
||||
// WebSocket Gateway
|
||||
NotificationsGateway,
|
||||
|
||||
// CQRS
|
||||
...CommandHandlers,
|
||||
|
||||
// Event Listeners
|
||||
...EventListeners,
|
||||
],
|
||||
exports: [EmailService, FcmService, TemplateService],
|
||||
exports: [EmailService, FcmService, StringeeSmsService, ZaloOaService, TemplateService, NotificationsGateway],
|
||||
})
|
||||
export class NotificationsModule {}
|
||||
|
||||
Reference in New Issue
Block a user