218 lines
8.4 KiB
TypeScript
218 lines
8.4 KiB
TypeScript
|
|
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 "../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, req);
|
||
|
|
}
|
||
|
|
|
||
|
|
@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, req);
|
||
|
|
}
|
||
|
|
|
||
|
|
@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);
|
||
|
|
}
|
||
|
|
|
||
|
|
@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);
|
||
|
|
}
|
||
|
|
}
|