Assist_Design/apps/bff/src/modules/auth/presentation/http/get-started.controller.ts

178 lines
5.6 KiB
TypeScript

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<SendVerificationCodeResponseDto> {
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<VerifyCodeResponseDto> {
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<QuickEligibilityResponseDto> {
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<MaybeLaterResponseDto> {
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,
},
};
}
}