2025-09-24 18:00:49 +09:00
|
|
|
import { Controller, Post, Body, UseGuards, Get, Req, HttpCode, UsePipes } from "@nestjs/common";
|
2025-09-18 12:34:26 +09:00
|
|
|
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";
|
2025-09-24 18:00:49 +09:00
|
|
|
import { ZodValidationPipe } from "@bff/core/validation";
|
2025-09-18 12:34:26 +09:00
|
|
|
|
|
|
|
|
// Import Zod schemas from domain
|
|
|
|
|
import {
|
2025-09-19 16:34:10 +09:00
|
|
|
signupRequestSchema,
|
|
|
|
|
passwordResetRequestSchema,
|
|
|
|
|
passwordResetSchema,
|
|
|
|
|
setPasswordRequestSchema,
|
|
|
|
|
linkWhmcsRequestSchema,
|
|
|
|
|
changePasswordRequestSchema,
|
|
|
|
|
validateSignupRequestSchema,
|
|
|
|
|
accountStatusRequestSchema,
|
|
|
|
|
ssoLinkRequestSchema,
|
|
|
|
|
checkPasswordNeededRequestSchema,
|
|
|
|
|
type SignupRequestInput,
|
|
|
|
|
type PasswordResetRequestInput,
|
|
|
|
|
type PasswordResetInput,
|
|
|
|
|
type SetPasswordRequestInput,
|
|
|
|
|
type LinkWhmcsRequestInput,
|
|
|
|
|
type ChangePasswordRequestInput,
|
|
|
|
|
type ValidateSignupRequestInput,
|
|
|
|
|
type AccountStatusRequestInput,
|
|
|
|
|
type SsoLinkRequestInput,
|
|
|
|
|
type CheckPasswordNeededRequestInput,
|
2025-09-20 11:35:40 +09:00
|
|
|
refreshTokenRequestSchema,
|
|
|
|
|
type RefreshTokenRequestInput,
|
2025-09-18 12:34:26 +09:00
|
|
|
} from "@customer-portal/domain";
|
|
|
|
|
|
|
|
|
|
@ApiTags("auth")
|
|
|
|
|
@Controller("auth")
|
2025-09-19 17:37:46 +09:00
|
|
|
export class AuthController {
|
2025-09-18 12:34:26 +09:00
|
|
|
constructor(private authService: AuthService) {}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("validate-signup")
|
|
|
|
|
@UseGuards(AuthThrottleGuard)
|
|
|
|
|
@Throttle({ default: { limit: 10, ttl: 900000 } }) // 10 validations per 15 minutes per IP
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(validateSignupRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@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" })
|
2025-09-25 11:44:10 +09:00
|
|
|
async validateSignup(@Body() validateData: ValidateSignupRequestInput, @Req() req: Request) {
|
2025-09-18 12:34:26 +09:00
|
|
|
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 } })
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(signupRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@HttpCode(200)
|
|
|
|
|
@ApiOperation({ summary: "Validate full signup data without creating anything" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Preflight results with next action guidance" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async signupPreflight(@Body() signupData: SignupRequestInput) {
|
2025-09-18 12:34:26 +09:00
|
|
|
return this.authService.signupPreflight(signupData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("account-status")
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(accountStatusRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@ApiOperation({ summary: "Get account status by email" })
|
|
|
|
|
@ApiOkResponse({ description: "Account status" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async accountStatus(@Body() body: AccountStatusRequestInput) {
|
2025-09-18 12:34:26 +09:00
|
|
|
return this.authService.getAccountStatus(body.email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("signup")
|
|
|
|
|
@UseGuards(AuthThrottleGuard)
|
|
|
|
|
@Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 signups per 15 minutes per IP
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(signupRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@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" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async signup(@Body() signupData: SignupRequestInput, @Req() req: Request) {
|
2025-09-18 12:34:26 +09:00
|
|
|
return this.authService.signup(signupData, req);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
2025-09-19 17:37:46 +09:00
|
|
|
@UseGuards(LocalAuthGuard, AuthThrottleGuard)
|
|
|
|
|
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 login attempts per 15 minutes per IP+UA
|
2025-09-18 12:34:26 +09:00
|
|
|
@Post("login")
|
|
|
|
|
@ApiOperation({ summary: "Authenticate user" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Login successful" })
|
|
|
|
|
@ApiResponse({ status: 401, description: "Invalid credentials" })
|
2025-09-19 17:37:46 +09:00
|
|
|
@ApiResponse({ status: 429, description: "Too many login attempts" })
|
2025-09-25 13:21:11 +09:00
|
|
|
async login(@Req() req: Request & { user: { id: string; email: string; role: string } }) {
|
2025-09-18 12:34:26 +09:00
|
|
|
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" };
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 11:35:40 +09:00
|
|
|
@Public()
|
|
|
|
|
@Post("refresh")
|
|
|
|
|
@Throttle({ default: { limit: 10, ttl: 300000 } }) // 10 attempts per 5 minutes per IP
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(refreshTokenRequestSchema))
|
2025-09-20 11:35:40 +09:00
|
|
|
@ApiOperation({ summary: "Refresh access token using refresh token" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Token refreshed successfully" })
|
|
|
|
|
@ApiResponse({ status: 401, description: "Invalid refresh token" })
|
|
|
|
|
@ApiResponse({ status: 429, description: "Too many refresh attempts" })
|
2025-09-25 11:44:10 +09:00
|
|
|
async refreshToken(@Body() body: RefreshTokenRequestInput, @Req() req: Request) {
|
2025-09-20 11:35:40 +09:00
|
|
|
return this.authService.refreshTokens(body.refreshToken, {
|
|
|
|
|
deviceId: body.deviceId,
|
2025-09-25 11:44:10 +09:00
|
|
|
userAgent: req.headers["user-agent"],
|
2025-09-20 11:35:40 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 12:34:26 +09:00
|
|
|
@Public()
|
|
|
|
|
@Post("link-whmcs")
|
|
|
|
|
@UseGuards(AuthThrottleGuard)
|
|
|
|
|
@Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 attempts per 15 minutes per IP
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(linkWhmcsRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@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" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async linkWhmcs(@Body() linkData: LinkWhmcsRequestInput, @Req() _req: Request) {
|
2025-09-18 17:49:43 +09:00
|
|
|
return this.authService.linkWhmcsUser(linkData);
|
2025-09-18 12:34:26 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("set-password")
|
|
|
|
|
@UseGuards(AuthThrottleGuard)
|
2025-09-19 17:37:46 +09:00
|
|
|
@Throttle({ default: { limit: 3, ttl: 300000 } }) // 3 attempts per 5 minutes per IP+UA
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(setPasswordRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@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" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async setPassword(@Body() setPasswordData: SetPasswordRequestInput, @Req() _req: Request) {
|
2025-09-18 17:49:43 +09:00
|
|
|
return this.authService.setPassword(setPasswordData);
|
2025-09-18 12:34:26 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("check-password-needed")
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(checkPasswordNeededRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@HttpCode(200)
|
|
|
|
|
@ApiOperation({ summary: "Check if user needs to set password" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Password status checked" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async checkPasswordNeeded(@Body() data: CheckPasswordNeededRequestInput) {
|
2025-09-18 12:34:26 +09:00
|
|
|
return this.authService.checkPasswordNeeded(data.email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
@Post("request-password-reset")
|
|
|
|
|
@Throttle({ default: { limit: 5, ttl: 900000 } })
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(passwordResetRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@ApiOperation({ summary: "Request password reset email" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Reset email sent if account exists" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async requestPasswordReset(@Body() body: PasswordResetRequestInput) {
|
2025-09-18 12:34:26 +09:00
|
|
|
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 } })
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(passwordResetSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@ApiOperation({ summary: "Reset password with token" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Password reset successful" })
|
2025-09-24 18:00:49 +09:00
|
|
|
async resetPassword(@Body() body: PasswordResetInput) {
|
2025-09-18 12:34:26 +09:00
|
|
|
return this.authService.resetPassword(body.token, body.password);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Post("change-password")
|
|
|
|
|
@Throttle({ default: { limit: 5, ttl: 300000 } })
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(changePasswordRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@ApiOperation({ summary: "Change password (authenticated)" })
|
|
|
|
|
@ApiResponse({ status: 200, description: "Password changed successfully" })
|
|
|
|
|
async changePassword(
|
|
|
|
|
@Req() req: Request & { user: { id: string } },
|
2025-09-24 18:00:49 +09:00
|
|
|
@Body() body: ChangePasswordRequestInput
|
2025-09-18 12:34:26 +09:00
|
|
|
) {
|
2025-09-18 17:49:43 +09:00
|
|
|
return this.authService.changePassword(
|
|
|
|
|
req.user.id,
|
|
|
|
|
body.currentPassword,
|
|
|
|
|
body.newPassword,
|
|
|
|
|
req
|
|
|
|
|
);
|
2025-09-18 12:34:26 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Get("me")
|
|
|
|
|
@ApiOperation({ summary: "Get current authentication status" })
|
2025-09-25 13:21:11 +09:00
|
|
|
getAuthStatus(@Req() req: Request & { user: { id: string; email: string; role: string } }) {
|
2025-09-18 12:34:26 +09:00
|
|
|
// 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")
|
2025-09-24 18:00:49 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(ssoLinkRequestSchema))
|
2025-09-18 12:34:26 +09:00
|
|
|
@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(
|
2025-09-25 11:44:10 +09:00
|
|
|
@Req() req: Request & { user: { id: string } },
|
2025-09-24 18:00:49 +09:00
|
|
|
@Body() body: SsoLinkRequestInput
|
2025-09-18 12:34:26 +09:00
|
|
|
) {
|
|
|
|
|
const destination = body?.destination;
|
|
|
|
|
return this.authService.createSsoLink(req.user.id, destination);
|
|
|
|
|
}
|
|
|
|
|
}
|