feat: add email verification endpoints with token-based verification and SMTP integration

This commit is contained in:
JakkrapartXD 2026-01-30 14:53:50 +07:00
parent 9629f79c52
commit babccc4869
3 changed files with 153 additions and 2 deletions

View file

@ -12,8 +12,11 @@ import {
ChangePasswordRequest,
ChangePasswordResponse,
updateAvatarRequest,
updateAvatarResponse
updateAvatarResponse,
SendVerifyEmailResponse,
VerifyEmailResponse
} from '../types/user.types';
import nodemailer from 'nodemailer';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
@ -298,4 +301,110 @@ export class UserService {
} : undefined
};
}
/**
* Send verification email to user
*/
async sendVerifyEmail(token: string): Promise<SendVerifyEmailResponse> {
try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; email: string };
const user = await prisma.user.findUnique({
where: { id: decoded.id }
});
if (!user) throw new UnauthorizedError('User not found');
if (user.email_verified_at) throw new ValidationError('Email already verified');
// Generate verification token (expires in 24 hours)
const verifyToken = jwt.sign(
{ id: user.id, email: user.email, type: 'email_verify' },
config.jwt.secret,
{ expiresIn: '24h' }
);
// Create verification URL
const verifyURL = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/verify-email?token=${verifyToken}`;
// Create transporter
const transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
...(config.smtp.user && config.smtp.pass && {
auth: {
user: config.smtp.user,
pass: config.smtp.pass
}
})
});
// Send email
const mailOptions = {
from: config.smtp.from,
to: user.email,
subject: 'Email Verification - E-Learning Platform',
text: `Please verify your email address by clicking on the following link:\n\n${verifyURL}\n\nThis link will expire in 24 hours.\n\nIf you did not create an account, please ignore this email.\n`,
html: `
<h2>Email Verification</h2>
<p>Thank you for registering with E-Learning Platform.</p>
<p>Please click on the following link to verify your email address:</p>
<p><a href="${verifyURL}">${verifyURL}</a></p>
<p>This link will expire in 24 hours.</p>
<p>If you did not create an account, please ignore this email.</p>
`
};
await transporter.sendMail(mailOptions);
logger.info('Verification email sent', { email: user.email });
return {
code: 200,
message: 'Verification email sent successfully'
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired');
logger.error('Failed to send verification email', { error });
throw error;
}
}
/**
* Verify email with token
*/
async verifyEmail(verifyToken: string): Promise<VerifyEmailResponse> {
try {
const decoded = jwt.verify(verifyToken, config.jwt.secret) as { id: number; email: string; type: string };
if (decoded.type !== 'email_verify') {
throw new UnauthorizedError('Invalid verification token');
}
const user = await prisma.user.findUnique({
where: { id: decoded.id }
});
if (!user) throw new UnauthorizedError('User not found');
if (user.email_verified_at) throw new ValidationError('Email already verified');
// Update email_verified_at
await prisma.user.update({
where: { id: user.id },
data: { email_verified_at: new Date() }
});
logger.info('Email verified successfully', { userId: user.id, email: user.email });
return {
code: 200,
message: 'Email verified successfully'
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid verification token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired');
logger.error('Failed to verify email', { error });
throw error;
}
}
}