feat: implement secure email change workflow with verification token and event handling
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
RefreshTokenDto,
|
||||
VerifyEmailDto,
|
||||
ChangePasswordDto,
|
||||
ChangeEmailDto,
|
||||
} from './dto';
|
||||
import { UserRole, AuthProvider, UserStatus } from '@prisma/client';
|
||||
import { TwoFactorService } from './two-factor/two-factor.service';
|
||||
@@ -539,6 +540,44 @@ export class AuthService {
|
||||
secret: this.configService.get<string>('jwt.secret'),
|
||||
});
|
||||
|
||||
// Email-change token — swap user's email to the pending one
|
||||
if (payload.type === 'email-change') {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const newEmail = (payload.newEmail as string)?.toLowerCase();
|
||||
if (!newEmail) {
|
||||
throw new BadRequestException('Invalid verification token');
|
||||
}
|
||||
|
||||
// Ensure the email hasn't been claimed by another account in the meantime
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { email: newEmail },
|
||||
});
|
||||
if (existing && existing.id !== user.id) {
|
||||
throw new ConflictException('Email is already in use');
|
||||
}
|
||||
|
||||
if (user.email === newEmail && user.emailVerified) {
|
||||
return { message: 'Email is already verified' };
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
email: newEmail,
|
||||
emailVerified: true,
|
||||
emailVerifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return { message: 'Email changed and verified successfully' };
|
||||
}
|
||||
|
||||
if (payload.type !== 'email-verification') {
|
||||
throw new BadRequestException('Invalid verification token');
|
||||
}
|
||||
@@ -594,6 +633,72 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// REQUEST EMAIL CHANGE
|
||||
// ==========================================
|
||||
async requestEmailChange(userId: string, dto: ChangeEmailDto) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { userProfile: true, agentProfile: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
if (user.authProvider !== AuthProvider.LOCAL) {
|
||||
throw new BadRequestException(
|
||||
'Email cannot be changed for social login accounts',
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
throw new BadRequestException('Password is not set on this account');
|
||||
}
|
||||
|
||||
const passwordOk = await argon2.verify(user.password, dto.password);
|
||||
if (!passwordOk) {
|
||||
throw new BadRequestException('Incorrect password');
|
||||
}
|
||||
|
||||
const newEmail = dto.newEmail.toLowerCase().trim();
|
||||
if (newEmail === user.email) {
|
||||
throw new BadRequestException(
|
||||
'New email must be different from current email',
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { email: newEmail },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('Email is already in use');
|
||||
}
|
||||
|
||||
const token = this.jwtService.sign(
|
||||
{ sub: user.id, newEmail, type: 'email-change' },
|
||||
{
|
||||
secret: this.configService.get<string>('jwt.secret'),
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
|
||||
const profile =
|
||||
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
||||
|
||||
// Emit event to send the verification email to the NEW address
|
||||
this.eventEmitter.emit('user.email-change-requested', {
|
||||
newEmail,
|
||||
token,
|
||||
name: profile?.firstName,
|
||||
});
|
||||
|
||||
return {
|
||||
message:
|
||||
'A verification link has been sent to your new email. Please click the link to confirm the change.',
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RESEND VERIFICATION EMAIL
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user