platform-codebase/@packages/@infrastructure/email-client/src/service.ts
Lilith 58eabb6294 chore(src): 🔧 Update TypeScript files in src directory to maintain consistency across project
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-12 05:27:50 -08:00

157 lines
4.9 KiB
TypeScript

import { getServiceRegistry } from '@lilith/service-registry'
import { Injectable, Logger, Inject, Optional } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import axios, { AxiosInstance } from 'axios'
import { EMAIL_CLIENT_OPTIONS } from './module'
import type {
EmailClientModuleOptions,
EmailUserData,
SendCustomEmailOptions,
SendTemplateEmailOptions,
} from './types'
/**
* Email client for communicating with the email backend service.
* Uses the internal API endpoints protected by X-Internal-Api-Key.
*
* All methods are graceful -- email failures never throw, they log and return null.
* This ensures email delivery issues never break calling service flows.
*/
@Injectable()
export class EmailClientService {
private readonly logger = new Logger(EmailClientService.name)
private readonly client: AxiosInstance
private readonly enabled: boolean
constructor(
@Inject(ConfigService) private readonly configService: ConfigService,
@Optional() @Inject(EMAIL_CLIENT_OPTIONS) private readonly options?: EmailClientModuleOptions
) {
const registry = getServiceRegistry()
const registryUrl = registry.getApiUrl('email') ?? ''
const serviceUrl = this.options?.serviceUrl
|| this.configService.get<string>('EMAIL_SERVICE_URL')
|| registryUrl
const apiKeyEnvVar = this.options?.apiKeyEnvVar ?? 'EMAIL_INTERNAL_API_KEY'
const apiKey = this.configService.get<string>(apiKeyEnvVar, '')
this.enabled = !!apiKey
if (!this.enabled) {
this.logger.warn(`${apiKeyEnvVar} not set - email notifications disabled`)
}
this.client = axios.create({
baseURL: `${serviceUrl}/internal`,
headers: {
'Content-Type': 'application/json',
'X-Internal-Api-Key': apiKey,
},
timeout: 10000,
})
}
private async post<T extends object>(endpoint: string, data: T): Promise<string | null> {
if (!this.enabled) {
this.logger.debug(`Email disabled, skipping: ${endpoint}`)
return null
}
try {
const response = await this.client.post(endpoint, data)
return response.data.jobId
} catch (error) {
this.logger.error(`Failed to send email via ${endpoint}:`, error)
return null
}
}
/**
* Send welcome email to new user
*/
async sendWelcome(data: EmailUserData): Promise<string | null> {
this.logger.log(`Sending welcome email to ${data.email}`)
return this.post('/send/welcome', data)
}
/**
* Send email verification link
*/
async sendVerification(
data: EmailUserData & { verificationToken: string }
): Promise<string | null> {
this.logger.log(`Sending verification email to ${data.email}`)
return this.post('/send/verification', data)
}
/**
* Send password reset link.
* Accepts resetToken (NOT resetUrl) to match the email service internal API contract.
*/
async sendPasswordReset(
data: EmailUserData & { resetToken: string; expiresInMinutes?: number }
): Promise<string | null> {
this.logger.log(`Sending password reset email to ${data.email}`)
return this.post('/send/password-reset', data)
}
/**
* Send password changed confirmation
*/
async sendPasswordChanged(data: EmailUserData): Promise<string | null> {
this.logger.log(`Sending password changed email to ${data.email}`)
return this.post('/send/password-changed', data)
}
/**
* Send account locked notification
*/
async sendAccountLocked(
data: EmailUserData & { reason?: string; unlockUrl?: string }
): Promise<string | null> {
this.logger.log(`Sending account locked email to ${data.email}`)
return this.post('/send/account-locked', data)
}
/**
* Send new login alert
*/
async sendLoginAlert(
data: EmailUserData & {
device?: string
location?: string
ipAddress?: string
}
): Promise<string | null> {
this.logger.log(`Sending login alert to ${data.email}`)
return this.post('/send/login-alert', data)
}
/**
* Send OTP code for MFA
*/
async sendOtp(
data: EmailUserData & { code: string; expiresInMinutes?: number }
): Promise<string | null> {
this.logger.log(`Sending OTP to ${data.email}`)
return this.post('/send/otp', data)
}
/**
* Send email using a named template with variable substitution.
* This is the generic method for sending any template-based email.
*/
async sendTemplate(options: SendTemplateEmailOptions): Promise<string | null> {
this.logger.log(`Sending template email "${options.templateName}" to ${options.to}`)
return this.post('/send/template', options)
}
/**
* Send a custom email with raw HTML content.
* Use this when no template exists for the email type.
*/
async sendCustom(options: SendCustomEmailOptions): Promise<string | null> {
this.logger.log(`Sending custom email "${options.subject}" to ${options.to}`)
return this.post('/send/custom', options)
}
}