diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 6fae38d..bb60798 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -25,6 +25,7 @@ import { AuthUserPayload } from './types/auth-user.type'; import { GoogleProfile } from './strategies/google.strategy'; import { UserRole } from '../types/prisma.types'; import { Request } from 'express'; +import { VerifyEmailDto } from '../users/dto/email-change.dto'; @Controller('auth') export class AuthController { @@ -188,4 +189,11 @@ export class AuthController { getLoginStatus(@Body() data: { email: string }) { return this.authService.getLoginStatus(data.email); } + + @Post('verify-email') + verifyEmail(@Body() verifyEmailDto: VerifyEmailDto, @Req() request: Request) { + const ipAddress = request.ip || request.socket.remoteAddress; + const userAgent = request.headers['user-agent']; + return this.authService.verifyInitialEmail(verifyEmailDto.token, ipAddress, userAgent); + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7df0cb8..963145a 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -119,6 +119,12 @@ export class AuthService { } const passwordHash = await hashPassword(data.password, this.bcryptRounds); + const verificationToken = randomToken(32); + const verificationExpiresAt = new Date(Date.now() + parseDuration( + this.configService.get('EMAIL_VERIFICATION_EXPIRES_IN') ?? '24h', + 24 * 60 * 60, + ) * 1000); + const user = await this.prisma.user.create({ data: { email: data.email, @@ -126,6 +132,8 @@ export class AuthService { firstName: data.firstName, lastName: data.lastName, phone: data.phone, + emailVerificationToken: verificationToken, + emailVerificationExpires: verificationExpiresAt, passwordHistory: { create: { passwordHash, @@ -134,11 +142,23 @@ export class AuthService { }, }); - const tokens = await this.issueTokenPair(user); + // Send verification email + await this.emailService + .sendEmail({ + to: user.email, + subject: 'Verify your email - PropChain', + template: 'email-verification', + context: { token: verificationToken }, + userId: user.id, + emailType: 'email_verification', + }) + .catch((err) => { + this.logger.error('Failed to queue verification email:', err?.message || err); + }); return { user: sanitizeUser(user), - ...tokens, + message: 'Registration successful. Please check your email to verify your account.', }; } @@ -187,6 +207,10 @@ export class AuthService { ); } + if (!user.isVerified) { + throw new UnauthorizedException('Please verify your email before logging in.'); + } + const passwordMatches = await comparePassword(data.password, user.password ?? ''); if (!passwordMatches) { // Record failed login attempt @@ -1090,7 +1114,7 @@ export class AuthService { }; } - private async issueTokenPair( + async issueTokenPair( user: Prisma.User, tokenFamily?: string, ipAddress?: string, @@ -1431,4 +1455,54 @@ export class AuthService { return false; } } + + async verifyInitialEmail(token: string, ipAddress?: string, userAgent?: string) { + // Find user by verification token + const user = await this.prisma.user.findFirst({ + where: { + emailVerificationToken: token, + }, + }); + + if (!user) { + throw new BadRequestException('Invalid or expired verification token'); + } + + // Check if token is expired + if (!user.emailVerificationExpires || new Date() > user.emailVerificationExpires) { + // Clear expired token + await this.prisma.user.update({ + where: { id: user.id }, + data: { + emailVerificationToken: null, + emailVerificationExpires: null, + }, + }); + throw new BadRequestException('Verification token has expired'); + } + + // Verify user not already verified + if (user.isVerified) { + throw new BadRequestException('Email already verified'); + } + + // Update user to isVerified and clear verification fields + const updatedUser = await this.prisma.user.update({ + where: { id: user.id }, + data: { + isVerified: true, + emailVerificationToken: null, + emailVerificationExpires: null, + }, + }); + + // Issue token pair + const tokens = await this.issueTokenPair(updatedUser, undefined, ipAddress, userAgent); + + return { + message: 'Email verified successfully', + user: sanitizeUser(updatedUser), + ...tokens, + }; + } }