import { Controller, Post, Body, UseGuards, Res, Req, HttpCode } from "@nestjs/common"; import type { Request, Response } from "express"; import { createZodDto } from "nestjs-zod"; import { RateLimitGuard, RateLimit, getRequestFingerprint } from "@bff/core/rate-limiting/index.js"; import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js"; import { Public } from "../../decorators/public.decorator.js"; import { sendVerificationCodeRequestSchema, sendVerificationCodeResponseSchema, verifyCodeRequestSchema, verifyCodeResponseSchema, quickEligibilityRequestSchema, quickEligibilityResponseSchema, completeAccountRequestSchema, maybeLaterRequestSchema, maybeLaterResponseSchema, } from "@customer-portal/domain/get-started"; import { GetStartedWorkflowService } from "../../infra/workflows/get-started-workflow.service.js"; // DTO classes using Zod schemas class SendVerificationCodeRequestDto extends createZodDto(sendVerificationCodeRequestSchema) {} class SendVerificationCodeResponseDto extends createZodDto(sendVerificationCodeResponseSchema) {} class VerifyCodeRequestDto extends createZodDto(verifyCodeRequestSchema) {} class VerifyCodeResponseDto extends createZodDto(verifyCodeResponseSchema) {} class QuickEligibilityRequestDto extends createZodDto(quickEligibilityRequestSchema) {} class QuickEligibilityResponseDto extends createZodDto(quickEligibilityResponseSchema) {} class CompleteAccountRequestDto extends createZodDto(completeAccountRequestSchema) {} class MaybeLaterRequestDto extends createZodDto(maybeLaterRequestSchema) {} class MaybeLaterResponseDto extends createZodDto(maybeLaterResponseSchema) {} const ACCESS_COOKIE_PATH = "/api"; const REFRESH_COOKIE_PATH = "/api/auth/refresh"; const TOKEN_TYPE = "Bearer" as const; const calculateCookieMaxAge = (isoTimestamp: string): number => { const expiresAt = Date.parse(isoTimestamp); if (Number.isNaN(expiresAt)) { return 0; } return Math.max(0, expiresAt - Date.now()); }; /** * Get Started Controller * * Handles the unified "Get Started" flow: * - Email verification via OTP * - Account status detection * - Quick eligibility check (guest) * - Account completion (SF-only users) * * All endpoints are public (no authentication required) */ @Controller("auth/get-started") export class GetStartedController { constructor(private readonly workflow: GetStartedWorkflowService) {} /** * Send OTP verification code to email * * Rate limit: 5 codes per 5 minutes per IP */ @Public() @Post("send-code") @HttpCode(200) @UseGuards(RateLimitGuard) @RateLimit({ limit: 5, ttl: 300 }) async sendVerificationCode( @Body() body: SendVerificationCodeRequestDto, @Req() req: Request ): Promise { const fingerprint = getRequestFingerprint(req); return this.workflow.sendVerificationCode(body, fingerprint); } /** * Verify OTP code and determine account status * * Rate limit: 10 attempts per 5 minutes per IP */ @Public() @Post("verify-code") @HttpCode(200) @UseGuards(RateLimitGuard) @RateLimit({ limit: 10, ttl: 300 }) async verifyCode( @Body() body: VerifyCodeRequestDto, @Req() req: Request ): Promise { const fingerprint = getRequestFingerprint(req); return this.workflow.verifyCode(body, fingerprint); } /** * Quick eligibility check for guests * Creates SF Account + eligibility case * * Rate limit: 5 per 15 minutes per IP */ @Public() @Post("quick-check") @HttpCode(200) @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 900 }) async quickEligibilityCheck( @Body() body: QuickEligibilityRequestDto ): Promise { return this.workflow.quickEligibilityCheck(body); } /** * "Maybe Later" flow * Creates SF Account + eligibility case, sends confirmation email * * Rate limit: 3 per 10 minutes per IP */ @Public() @Post("maybe-later") @HttpCode(200) @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) @RateLimit({ limit: 3, ttl: 600 }) async maybeLater(@Body() body: MaybeLaterRequestDto): Promise { return this.workflow.maybeLater(body); } /** * Complete account for SF-only users * Creates WHMCS client and Portal user, links to existing SF account * * Returns auth tokens (sets httpOnly cookies) * * Rate limit: 5 per 15 minutes per IP */ @Public() @Post("complete-account") @HttpCode(200) @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 900 }) async completeAccount( @Body() body: CompleteAccountRequestDto, @Res({ passthrough: true }) res: Response ) { const result = await this.workflow.completeAccount(body); // Set auth cookies (same pattern as signup) const accessExpires = result.tokens.expiresAt; const refreshExpires = result.tokens.refreshExpiresAt; res.cookie("access_token", result.tokens.accessToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: ACCESS_COOKIE_PATH, maxAge: calculateCookieMaxAge(accessExpires), }); res.cookie("refresh_token", result.tokens.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: REFRESH_COOKIE_PATH, maxAge: calculateCookieMaxAge(refreshExpires), }); return { user: result.user, session: { expiresAt: accessExpires, refreshExpiresAt: refreshExpires, tokenType: TOKEN_TYPE, }, }; } }