import { Controller, Post, Body, UseGuards, Get, Req, HttpCode } from "@nestjs/common"; import type { Request } from "express"; import { Throttle } from "@nestjs/throttler"; import { AuthService } from "./auth.service"; import { LocalAuthGuard } from "./guards/local-auth.guard"; import { AuthThrottleGuard } from "./guards/auth-throttle.guard"; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse } from "@nestjs/swagger"; import { Public } from "./decorators/public.decorator"; import { ZodPipe } from "@bff/core/validation"; // Import Zod schemas from domain import { bffSignupSchema, bffLoginSchema, bffPasswordResetRequestSchema, bffPasswordResetSchema, bffSetPasswordSchema, bffLinkWhmcsSchema, bffChangePasswordSchema, bffValidateSignupSchema, bffAccountStatusRequestSchema, bffSsoLinkSchema, bffCheckPasswordNeededSchema, type BffSignupData, type BffLoginData, type BffPasswordResetRequestData, type BffPasswordResetData, type BffSetPasswordData, type BffLinkWhmcsData, type BffChangePasswordData, type BffValidateSignupData, type BffAccountStatusRequestData, type BffSsoLinkData, type BffCheckPasswordNeededData, } from "@customer-portal/domain"; @ApiTags("auth") @Controller("auth") export class AuthZodController { constructor(private authService: AuthService) {} @Public() @Post("validate-signup") @UseGuards(AuthThrottleGuard) @Throttle({ default: { limit: 10, ttl: 900000 } }) // 10 validations per 15 minutes per IP @ApiOperation({ summary: "Validate customer number for signup" }) @ApiResponse({ status: 200, description: "Validation successful" }) @ApiResponse({ status: 409, description: "Customer already has account" }) @ApiResponse({ status: 400, description: "Customer number not found" }) @ApiResponse({ status: 429, description: "Too many validation attempts" }) async validateSignup( @Body(ZodPipe(bffValidateSignupSchema)) validateData: BffValidateSignupData, @Req() req: Request ) { return this.authService.validateSignup(validateData, req); } @Public() @Get("health-check") @ApiOperation({ summary: "Check auth service health and integrations" }) @ApiResponse({ status: 200, description: "Health check results" }) async healthCheck() { return this.authService.healthCheck(); } @Public() @Post("signup-preflight") @UseGuards(AuthThrottleGuard) @Throttle({ default: { limit: 10, ttl: 900000 } }) @HttpCode(200) @ApiOperation({ summary: "Validate full signup data without creating anything" }) @ApiResponse({ status: 200, description: "Preflight results with next action guidance" }) async signupPreflight(@Body(ZodPipe(bffSignupSchema)) signupData: BffSignupData) { return this.authService.signupPreflight(signupData); } @Public() @Post("account-status") @ApiOperation({ summary: "Get account status by email" }) @ApiOkResponse({ description: "Account status" }) async accountStatus(@Body(ZodPipe(bffAccountStatusRequestSchema)) body: BffAccountStatusRequestData) { return this.authService.getAccountStatus(body.email); } @Public() @Post("signup") @UseGuards(AuthThrottleGuard) @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 signups per 15 minutes per IP @ApiOperation({ summary: "Create new user account" }) @ApiResponse({ status: 201, description: "User created successfully" }) @ApiResponse({ status: 409, description: "User already exists" }) @ApiResponse({ status: 429, description: "Too many signup attempts" }) async signup(@Body(ZodPipe(bffSignupSchema)) signupData: BffSignupData, @Req() req: Request) { return this.authService.signup(signupData, req); } @Public() @UseGuards(LocalAuthGuard) @Post("login") @ApiOperation({ summary: "Authenticate user" }) @ApiResponse({ status: 200, description: "Login successful" }) @ApiResponse({ status: 401, description: "Invalid credentials" }) async login(@Req() req: Request & { user: { id: string; email: string; role?: string } }) { return this.authService.login(req.user, req); } @Post("logout") @ApiOperation({ summary: "Logout user" }) @ApiResponse({ status: 200, description: "Logout successful" }) async logout(@Req() req: Request & { user: { id: string } }) { const authHeader = req.headers.authorization as string | string[] | undefined; let bearer: string | undefined; if (typeof authHeader === "string") { bearer = authHeader; } else if (Array.isArray(authHeader) && authHeader.length > 0) { bearer = authHeader[0]; } const token = bearer?.startsWith("Bearer ") ? bearer.slice(7) : undefined; await this.authService.logout(req.user.id, token ?? "", req); return { message: "Logout successful" }; } @Public() @Post("link-whmcs") @UseGuards(AuthThrottleGuard) @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 attempts per 15 minutes per IP @ApiOperation({ summary: "Link existing WHMCS user" }) @ApiResponse({ status: 200, description: "WHMCS account linked successfully", }) @ApiResponse({ status: 401, description: "Invalid WHMCS credentials" }) @ApiResponse({ status: 429, description: "Too many link attempts" }) async linkWhmcs(@Body(ZodPipe(bffLinkWhmcsSchema)) linkData: BffLinkWhmcsData, @Req() _req: Request) { return this.authService.linkWhmcsUser(linkData); } @Public() @Post("set-password") @UseGuards(AuthThrottleGuard) @Throttle({ default: { limit: 3, ttl: 300000 } }) // 3 attempts per 5 minutes per IP @ApiOperation({ summary: "Set password for linked user" }) @ApiResponse({ status: 200, description: "Password set successfully" }) @ApiResponse({ status: 401, description: "User not found" }) @ApiResponse({ status: 429, description: "Too many password attempts" }) async setPassword(@Body(ZodPipe(bffSetPasswordSchema)) setPasswordData: BffSetPasswordData, @Req() _req: Request) { return this.authService.setPassword(setPasswordData); } @Public() @Post("check-password-needed") @HttpCode(200) @ApiOperation({ summary: "Check if user needs to set password" }) @ApiResponse({ status: 200, description: "Password status checked" }) async checkPasswordNeeded(@Body(ZodPipe(bffCheckPasswordNeededSchema)) data: BffCheckPasswordNeededData) { return this.authService.checkPasswordNeeded(data.email); } @Public() @Post("request-password-reset") @Throttle({ default: { limit: 5, ttl: 900000 } }) @ApiOperation({ summary: "Request password reset email" }) @ApiResponse({ status: 200, description: "Reset email sent if account exists" }) async requestPasswordReset(@Body(ZodPipe(bffPasswordResetRequestSchema)) body: BffPasswordResetRequestData) { await this.authService.requestPasswordReset(body.email); return { message: "If an account exists, a reset email has been sent" }; } @Public() @Post("reset-password") @Throttle({ default: { limit: 5, ttl: 900000 } }) @ApiOperation({ summary: "Reset password with token" }) @ApiResponse({ status: 200, description: "Password reset successful" }) async resetPassword(@Body(ZodPipe(bffPasswordResetSchema)) body: BffPasswordResetData) { return this.authService.resetPassword(body.token, body.password); } @Post("change-password") @Throttle({ default: { limit: 5, ttl: 300000 } }) @ApiOperation({ summary: "Change password (authenticated)" }) @ApiResponse({ status: 200, description: "Password changed successfully" }) async changePassword( @Req() req: Request & { user: { id: string } }, @Body(ZodPipe(bffChangePasswordSchema)) body: BffChangePasswordData ) { return this.authService.changePassword( req.user.id, body.currentPassword, body.newPassword, req ); } @Get("me") @ApiOperation({ summary: "Get current authentication status" }) getAuthStatus(@Req() req: Request & { user: { id: string; email: string; role?: string } }) { // Return basic auth info only - full profile should use /api/me return { isAuthenticated: true, user: { id: req.user.id, email: req.user.email, role: req.user.role, }, }; } @Post("sso-link") @ApiOperation({ summary: "Create SSO link to WHMCS" }) @ApiResponse({ status: 200, description: "SSO link created successfully" }) @ApiResponse({ status: 404, description: "User not found or not linked to WHMCS", }) async createSsoLink( @Req() req: Request & { user: { id: string } }, @Body(ZodPipe(bffSsoLinkSchema)) body: BffSsoLinkData ) { const destination = body?.destination; return this.authService.createSsoLink(req.user.id, destination); } }