feat(email): add internal module with OTP email support
Add internal email module for service-to-service communication: - InternalModule and InternalController for internal API endpoints - OTP code email template for authentication flows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0e549dc60
commit
e78b9c4543
4 changed files with 217 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ import { OrdersModule } from './orders/orders.module'
|
|||
import { UsersEmailModule } from './users/users-email.module'
|
||||
import { EmployeesModule } from './employees/employees.module'
|
||||
import { AdminModule } from './admin/admin.module'
|
||||
import { InternalModule } from './internal/internal.module'
|
||||
import { HealthController } from './health.controller'
|
||||
|
||||
@Module({
|
||||
|
|
@ -75,6 +76,7 @@ import { HealthController } from './health.controller'
|
|||
UsersEmailModule,
|
||||
EmployeesModule,
|
||||
AdminModule,
|
||||
InternalModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
|
|
|
|||
176
features/email/backend/src/internal/internal.controller.ts
Normal file
176
features/email/backend/src/internal/internal.controller.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Headers,
|
||||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { ApiTags, ApiOperation, ApiHeader } from '@nestjs/swagger'
|
||||
|
||||
import { UsersEmailService } from '../users/users-email.service'
|
||||
import { EmailQueueService } from '../core/email-queue.service'
|
||||
import { EmailCategory } from '../core/entities/email-log.entity'
|
||||
|
||||
/**
|
||||
* Internal API for service-to-service email requests.
|
||||
* Protected by X-Internal-Api-Key header.
|
||||
*/
|
||||
@ApiTags('Internal')
|
||||
@Controller('internal')
|
||||
export class InternalController {
|
||||
private readonly logger = new Logger(InternalController.name)
|
||||
private readonly internalApiKey: string
|
||||
|
||||
constructor(
|
||||
private readonly usersEmailService: UsersEmailService,
|
||||
private readonly emailQueue: EmailQueueService,
|
||||
private readonly configService: ConfigService
|
||||
) {
|
||||
this.internalApiKey = this.configService.get('INTERNAL_API_KEY', '')
|
||||
}
|
||||
|
||||
private validateApiKey(apiKey: string | undefined): void {
|
||||
if (!this.internalApiKey) {
|
||||
this.logger.warn('INTERNAL_API_KEY not configured - internal API disabled')
|
||||
throw new UnauthorizedException('Internal API not configured')
|
||||
}
|
||||
if (apiKey !== this.internalApiKey) {
|
||||
throw new UnauthorizedException('Invalid API key')
|
||||
}
|
||||
}
|
||||
|
||||
@Post('send/welcome')
|
||||
@ApiOperation({ summary: 'Send welcome email to new user' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendWelcome(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body() body: { userId: string; email: string; name?: string }
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: welcome email for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendWelcomeEmail(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/verification')
|
||||
@ApiOperation({ summary: 'Send email verification link' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendVerification(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body()
|
||||
body: { userId: string; email: string; name?: string; verificationToken: string }
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: verification email for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendEmailVerification(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/password-reset')
|
||||
@ApiOperation({ summary: 'Send password reset link' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendPasswordReset(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body() body: { userId: string; email: string; name?: string; resetToken: string }
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: password reset for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendPasswordReset(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/password-changed')
|
||||
@ApiOperation({ summary: 'Send password changed confirmation' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendPasswordChanged(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body() body: { userId: string; email: string; name?: string }
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: password changed for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendPasswordChanged(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/account-locked')
|
||||
@ApiOperation({ summary: 'Send account locked notification' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendAccountLocked(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body()
|
||||
body: {
|
||||
userId: string
|
||||
email: string
|
||||
name?: string
|
||||
reason?: string
|
||||
unlockUrl?: string
|
||||
}
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: account locked for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendAccountLocked(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/login-alert')
|
||||
@ApiOperation({ summary: 'Send new login alert' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendLoginAlert(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body()
|
||||
body: {
|
||||
userId: string
|
||||
email: string
|
||||
name?: string
|
||||
device?: string
|
||||
location?: string
|
||||
ipAddress?: string
|
||||
}
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: login alert for ${body.email}`)
|
||||
|
||||
const jobId = await this.usersEmailService.sendLoginAlert(body)
|
||||
return { jobId }
|
||||
}
|
||||
|
||||
@Post('send/otp')
|
||||
@ApiOperation({ summary: 'Send OTP code for MFA' })
|
||||
@ApiHeader({ name: 'X-Internal-Api-Key', required: true })
|
||||
async sendOtp(
|
||||
@Headers('x-internal-api-key') apiKey: string,
|
||||
@Body()
|
||||
body: {
|
||||
userId: string
|
||||
email: string
|
||||
name?: string
|
||||
code: string
|
||||
expiresInMinutes?: number
|
||||
}
|
||||
): Promise<{ jobId: string }> {
|
||||
this.validateApiKey(apiKey)
|
||||
this.logger.log(`Internal request: OTP for ${body.email}`)
|
||||
|
||||
const jobId = await this.emailQueue.queueEmail({
|
||||
to: body.email,
|
||||
templateName: 'otp-code',
|
||||
variables: {
|
||||
name: body.name || 'there',
|
||||
code: body.code,
|
||||
expiresIn: `${body.expiresInMinutes || 10} minutes`,
|
||||
},
|
||||
category: EmailCategory.USERS,
|
||||
userId: body.userId,
|
||||
priority: 'high',
|
||||
})
|
||||
|
||||
return { jobId }
|
||||
}
|
||||
}
|
||||
15
features/email/backend/src/internal/internal.module.ts
Normal file
15
features/email/backend/src/internal/internal.module.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Module } from '@nestjs/common'
|
||||
|
||||
import { CoreModule } from '../core/core.module'
|
||||
import { UsersEmailModule } from '../users/users-email.module'
|
||||
import { InternalController } from './internal.controller'
|
||||
|
||||
/**
|
||||
* Internal API module for service-to-service communication.
|
||||
* Provides endpoints that other platform services can call to send emails.
|
||||
*/
|
||||
@Module({
|
||||
imports: [CoreModule, UsersEmailModule],
|
||||
controllers: [InternalController],
|
||||
})
|
||||
export class InternalModule {}
|
||||
24
features/email/backend/templates/users/otp-code.hbs
Normal file
24
features/email/backend/templates/users/otp-code.hbs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{{!-- templates/users/otp-code.hbs --}}
|
||||
{{!-- Variables: name, code, expiresIn --}}
|
||||
|
||||
<h1>Your Verification Code</h1>
|
||||
|
||||
<p>Hi {{name}},</p>
|
||||
|
||||
<p>Here's your one-time verification code:</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #FF69B4 0%, #FF1493 100%); color: white; font-size: 32px; font-weight: bold; letter-spacing: 8px; text-align: center; padding: 24px 40px; border-radius: 12px; margin: 24px 0;">
|
||||
{{code}}
|
||||
</div>
|
||||
|
||||
<p>This code will expire in <strong>{{expiresIn}}</strong>.</p>
|
||||
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
If you didn't request this code, you can safely ignore this email.
|
||||
Someone may have entered your email address by mistake.
|
||||
</p>
|
||||
|
||||
<p style="color: #666; font-size: 14px; margin-top: 24px;">
|
||||
<strong>Security tip:</strong> Never share this code with anyone.
|
||||
Lilith staff will never ask for your verification code.
|
||||
</p>
|
||||
Loading…
Add table
Reference in a new issue