import { Controller, Post, Body, Res, BadRequestException, Logger, Req, UseGuards,Get } from '@nestjs/common'; import { AuthService } from './auth.service'; import { SignupDto } from '../../common/dto/signup.dto'; import { LoginDto } from '../../common/dto/login.dto'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody, ApiExcludeEndpoint, ApiOkResponse } from '@nestjs/swagger'; import { JwtAuthGuard } from './jwt-auth.guard'; import { Request } from 'express'; import { VerifyOtpDto } from '../../common/dto/verify-otp.dto'; import { AuthGuard } from '@nestjs/passport'; import { Response } from 'express'; import { ConfigService } from '@nestjs/config'; @ApiTags('Auth') @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); // declare logger here constructor( private readonly authService: AuthService, private readonly config: ConfigService, ) {} @Post('signup') @ApiOperation({ summary: 'Create a new user' }) @ApiBody({ description: 'Signup payload', type: SignupDto, examples: { valid: { summary: 'Valid input', value: { email: 'test@example.com', password: 'MyPass@123', name: 'John Doe' }, }, invalidEmail: { summary: 'Invalid email format', value: { email: 'testexample.com', password: 'MyPass@123', name: 'John Doe' }, }, invalidPassword: { summary: 'Invalid password (no uppercase/special char)', value: { email: 'test@example.com', password: 'mypassword', name: 'John Doe' }, }, bothInvalid: { summary: 'Both email and password invalid', value: { email: 'testexample.com', password: 'mypassword', name: 'John Doe' }, }, }, }) @ApiResponse({ status: 201, description: 'User created successfully', schema: { example: { message: 'User created successfully', user: { id: 'uuid', email: 'test@example.com', name: 'John Doe' }, accessToken: 'jwt-access-token', refreshToken: 'jwt-refresh-token', }, }, }) @ApiResponse({ status: 400, description: 'Validation failed', schema: { example: { statusCode: 400, message: [ 'Email must be valid', 'Password must contain at least 1 uppercase letter and 1 special character', ], error: 'Bad Request', }, }, }) async signup(@Body() dto: SignupDto) { try { const { name, email, password } = dto; // 1️⃣ Email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { this.logger.warn(`Signup failed: Invalid email format: ${email}`); throw new BadRequestException('Invalid email format'); } // 2️⃣ Password strength validation const passwordRegex = /^(?=.*[A-Z])(?=.*[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]).{8,}$/; if (!passwordRegex.test(password)) { this.logger.warn( `Signup failed for email ${email}: Password does not meet strength requirements` ); throw new BadRequestException( 'Password must be at least 8 characters long, contain 1 uppercase letter, and 1 special character', ); } // 3️⃣ Check if user already exists const existingUser = await this.authService.findUserByEmail(email); if (existingUser) { this.logger.warn(`Signup failed: Email already registered: ${email}`); throw new BadRequestException('Email already registered'); } // 4️⃣ Create user const user = await this.authService.signup(name, email, password); this.logger.log(`User created successfully: ${email}`); return { message: 'User created successfully. Please check your email for the OTP to verify your account.', userId: user.user.id, }; } catch (error: unknown) { if (error instanceof Error) { this.logger.error(`Signup failed for email ${dto.email}: ${error.message}`); throw error; } else { this.logger.error(`Signup failed for email ${dto.email}: Unknown error`); throw new BadRequestException('Signup failed due to unknown error'); } } } @Post('login') @ApiOperation({ summary: 'User Login' }) @ApiBody({ description: 'Login payload', type: LoginDto, examples: { valid: { summary: 'Valid credentials', value: { email: 'test@example.com', password: 'MyPass@123' }, }, invalidEmail: { summary: 'Email not registered', value: { email: 'unknown@example.com', password: 'MyPass@123' }, }, invalidPassword: { summary: 'Wrong password', value: { email: 'test@example.com', password: 'wrongPass' }, }, }, }) @ApiResponse({ status: 200, description: 'Login successful', schema: { example: { message: 'Login successful', user: { id: 'uuid', email: 'test@example.com', name: 'John Doe', isLoggedin: true }, accessToken: 'jwt-access-token', refreshToken: 'jwt-refresh-token', }, }, }) @ApiResponse({ status: 401, description: 'Invalid credentials', schema: { example: { statusCode: 401, message: 'Invalid credentials', error: 'Unauthorized', }, }, }) async login(@Body() dto: LoginDto) { try { const { email, password } = dto; // Basic checks for empty email/password if (!email || !password) { this.logger.warn('Login failed: Email or password missing'); throw new BadRequestException('Email and password are required'); } const result = await this.authService.login(email, password); this.logger.log(`User logged in successfully: ${email}`); return result; } catch (error: unknown) { if (error instanceof Error) { this.logger.error(`Login failed for email ${dto.email}: ${error.message}`); throw error; } else { this.logger.error(`Login failed for email ${dto.email}: Unknown error`); throw new BadRequestException('Login failed due to unknown error'); } } } @Post('refresh') @ApiOperation({ summary: 'Refresh JWT tokens using refresh token' }) @ApiBody({ description: 'Refresh token payload', schema: { type: 'object', properties: { refreshToken: { type: 'string', example: 'refresh-token-here' }, userId: { type: 'string', example: 'uuid-of-user (optional; derived from token when omitted)' }, }, required: ['refreshToken'], }, }) @ApiResponse({ status: 200, description: 'Tokens refreshed successfully', schema: { example: { message: 'Tokens refreshed successfully', user: { id: 'uuid', email: 'test@example.com', name: 'John Doe' }, accessToken: 'new-jwt-access-token', refreshToken: 'new-jwt-refresh-token', }, }, }) async refresh(@Body() body: { userId?: string; refreshToken: string }) { const { userId, refreshToken } = body; return this.authService.refreshTokens(userId, refreshToken); } @Post('logout') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Logout user and revoke refresh token' }) async logout(@Req() req: Request) { // ✅ type req as Request const userId = (req.user as any).id; // user comes from JWT guard await this.authService.logout(userId); return { message: 'Logged out successfully' }; } @Post('verify-otp') @ApiOperation({ summary: 'Verify OTP sent to email' }) @ApiBody({ schema: { type: 'object', properties: { email: { type: 'string', example: 'johndoe@example.com' }, otp: { type: 'string', example: '123456' }, }, required: ['email', 'otp'], }, }) async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { return this.authService.verifyEmailOtp( verifyOtpDto.email, verifyOtpDto.otp, ); } @Post('resend-otp') @ApiOperation({ summary: 'Resend OTP to user email' }) @ApiBody({ schema: { type: 'object', properties: { email: { type: 'string', example: 'johndoe@example.com' }, }, required: ['email'], }, }) @ApiOperation({ summary: 'Resend OTP to user email' }) async resendOtp(@Body() body: { email: string }) { return this.authService.resendOtp(body.email); } //Initiate Google Login @Get('google') @ApiExcludeEndpoint() @UseGuards(AuthGuard('google')) async googleLogin(@Req() req: Request) { // This will redirect user to Google for login } // 2. Google callback route @Get('google/callback') @ApiExcludeEndpoint() @UseGuards(AuthGuard('google')) async googleCallback(@Req() req: Request, @Res() res: Response) { const user = req.user; if (!user) throw new BadRequestException('Google login failed'); const frontendUrl = this.config.get('FRONTEND_URL') || 'http://localhost:3000'; const accessToken = (user as any).accessToken; const refreshToken = (user as any).refreshToken; const name = (user as any).user?.name || ''; const email = (user as any).user?.email || ''; const redirectUrl = `${frontendUrl}/auth/google/callback` + `?accessToken=${encodeURIComponent(accessToken)}` + `&refreshToken=${encodeURIComponent(refreshToken)}` + `&name=${encodeURIComponent(name)}` + `&email=${encodeURIComponent(email)}`; res.redirect(redirectUrl); return; } @Get('me') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get current authenticated user', description: 'Returns the logged-in user\'s basic info. Call this immediately after login/verify-otp to get user details.', }) @ApiOkResponse({ description: 'Current user info', schema: { example: { id: 'c80d42ce-dfa3-41be-9676-599b58b93004', name: 'John Doe', email: 'john@example.com', isEmailVerified: true, }, }, }) async getMe(@Req() req: Request) { const user = req.user as any; return { id: user.id, name: user.name, email: user.email, isEmailVerified: user.isVerified, // Read live from the DB user the JWT strategy loaded, not from the token. // The admin UI uses it only to decide what to render; the API is the actual // gate, so a stale or forged value here grants nothing. role: user.role ?? 'candidate', }; } @Post('forgot-password') @ApiOperation({ summary: 'Send password reset link to user email' }) @ApiBody({ schema: { type: 'object', properties: { email: { type: 'string', example: 'user@example.com' }, }, required: ['email'], }, }) async forgotPassword(@Body() body: { email: string }) { await this.authService.forgotPassword(body.email); return { message: 'Password reset link sent to email' }; } @Post('reset-password') @ApiOperation({ summary: 'Reset password using token' }) @ApiBody({ schema: { type: 'object', properties: { email: { type: 'string', example: 'johndoe@example.com' }, token: { type: 'string', example: 'reset-token-from-email' }, newPassword: { type: 'string', example: 'NewPass@123' }, }, required: ['email', 'token', 'newPassword'], }, }) async resetPassword(@Body() body: { email: string; token: string; newPassword: string }) { return this.authService.resetPassword(body.email, body.token, body.newPassword); } }