// otp.controller.ts import { BadRequestException, Body, Controller, Post, } from "@nestjs/common"; import { OtpService, OtpTarget } from "./otp.service"; import { Public } from "@edr/api-common"; // At least one of phone/email must be present. When BOTH are given the code is // sent to both and either one verifies it — the caller no longer picks a single // channel, it just states every address it knows for the account. function toTarget(phone?: string, email?: string): OtpTarget { const target: OtpTarget = {}; if (email?.trim()) target.email = email; if (phone?.trim()) target.phone = phone; if (!target.email && !target.phone) { throw new BadRequestException("phone or email is required"); } return target; } // TODO: these public routes need per-target + per-IP rate limiting (a NestJS // ThrottlerGuard / @Throttle on /otp/send and /otp/verify). No Throttler is // wired into the app yet; add @nestjs/throttler and apply it here. @Controller("otp") @Public() export class OtpController { constructor( private readonly otpService: OtpService ) {} // --------------------------------------------------------------------------- // Send OTP // --------------------------------------------------------------------------- @Post("send") async sendOtp( @Body("phone") phone?: string, @Body("email") email?: string ) { // `delivered` stays server-side: this route is @Public(), and whether our // broker accepted the publish is infrastructure state an anonymous caller has // no need for. It is on the `otp.dispatch` log line instead. const { success, message } = await this.otpService.sendOtp( toTarget(phone, email) ); return { success, message }; } // --------------------------------------------------------------------------- // Verify OTP // --------------------------------------------------------------------------- @Post("verify") async verifyOtp( @Body("phone") phone: string | undefined, @Body("email") email: string | undefined, @Body("otp") otp: string ) { return this.otpService.verifyOtp( toTarget(phone, email), otp ); } }