File size: 11,482 Bytes
28406ee
8a24f2c
aa95439
 
9a951d7
cb824bf
b4d141a
 
ec9da6a
28406ee
d8ea863
28406ee
aa95439
8a24f2c
 
aa95439
d8ea863
 
 
 
aa95439
8a24f2c
 
aa95439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a24f2c
aa95439
9a951d7
 
 
 
aa95439
 
 
 
 
 
 
 
8a24f2c
aa95439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90c9777
aa95439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a24f2c
aa95439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a24f2c
e08c8c0
 
 
 
 
 
 
 
a0eafe5
e08c8c0
a0eafe5
e08c8c0
 
 
 
 
 
 
 
 
 
 
 
 
 
a0eafe5
e08c8c0
 
 
cb824bf
 
 
 
 
 
 
90c9777
cb824bf
 
b4d141a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a24f2c
ec9da6a
90c9777
9a951d7
ec9da6a
 
 
b4d141a
 
 
ec9da6a
 
9a951d7
ec9da6a
9eb0445
ec9da6a
 
d8ea863
 
 
 
 
 
 
 
 
 
 
 
 
 
9eb0445
ec9da6a
 
9a951d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86063f3
 
 
 
9a951d7
ec9da6a
9ca537f
 
90c9777
 
 
 
 
 
 
 
 
 
9ca537f
 
 
ec9da6a
9ca537f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec9da6a
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
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<string>('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);
  }
}