Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
80 changes: 77 additions & 3 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,21 @@ 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<string>('EMAIL_VERIFICATION_EXPIRES_IN') ?? '24h',
24 * 60 * 60,
) * 1000);

const user = await this.prisma.user.create({
data: {
email: data.email,
password: passwordHash,
firstName: data.firstName,
lastName: data.lastName,
phone: data.phone,
emailVerificationToken: verificationToken,
emailVerificationExpires: verificationExpiresAt,
passwordHistory: {
create: {
passwordHash,
Expand All @@ -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.',
};
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1090,7 +1114,7 @@ export class AuthService {
};
}

private async issueTokenPair(
async issueTokenPair(
user: Prisma.User,
tokenFamily?: string,
ipAddress?: string,
Expand Down Expand Up @@ -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,
};
}
}
Loading