feat: add email verification endpoints with token-based verification and SMTP integration
This commit is contained in:
parent
9629f79c52
commit
babccc4869
3 changed files with 153 additions and 2 deletions
|
|
@ -8,7 +8,9 @@ import {
|
||||||
ProfileUpdateResponse,
|
ProfileUpdateResponse,
|
||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
ChangePasswordResponse,
|
ChangePasswordResponse,
|
||||||
updateAvatarResponse
|
updateAvatarResponse,
|
||||||
|
SendVerifyEmailResponse,
|
||||||
|
VerifyEmailResponse
|
||||||
} from '../types/user.types';
|
} from '../types/user.types';
|
||||||
import { ChangePassword } from '../types/auth.types';
|
import { ChangePassword } from '../types/auth.types';
|
||||||
import { profileUpdateSchema, changePasswordSchema } from "../validators/user.validator";
|
import { profileUpdateSchema, changePasswordSchema } from "../validators/user.validator";
|
||||||
|
|
@ -106,4 +108,34 @@ export class UserController {
|
||||||
|
|
||||||
return await this.userService.uploadAvatarPicture(token, file);
|
return await this.userService.uploadAvatarPicture(token, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send verification email to user
|
||||||
|
* @summary Send email verification link to authenticated user's email
|
||||||
|
* @param request Express request object with JWT token in Authorization header
|
||||||
|
*/
|
||||||
|
@Post('send-verify-email')
|
||||||
|
@Security('jwt')
|
||||||
|
@SuccessResponse('200', 'Verification email sent successfully')
|
||||||
|
@Response('401', 'Invalid or expired token')
|
||||||
|
@Response('400', 'Email already verified')
|
||||||
|
public async sendVerifyEmail(@Request() request: any): Promise<SendVerifyEmailResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) throw new ValidationError('No token provided');
|
||||||
|
return await this.userService.sendVerifyEmail(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify email with token
|
||||||
|
* @summary Verify user's email address using verification token
|
||||||
|
* @param body Object containing the verification token
|
||||||
|
*/
|
||||||
|
@Post('verify-email')
|
||||||
|
@SuccessResponse('200', 'Email verified successfully')
|
||||||
|
@Response('401', 'Invalid or expired verification token')
|
||||||
|
@Response('400', 'Email already verified')
|
||||||
|
public async verifyEmail(@Body() body: { token: string }): Promise<VerifyEmailResponse> {
|
||||||
|
if (!body.token) throw new ValidationError('Verification token is required');
|
||||||
|
return await this.userService.verifyEmail(body.token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ import {
|
||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
ChangePasswordResponse,
|
ChangePasswordResponse,
|
||||||
updateAvatarRequest,
|
updateAvatarRequest,
|
||||||
updateAvatarResponse
|
updateAvatarResponse,
|
||||||
|
SendVerifyEmailResponse,
|
||||||
|
VerifyEmailResponse
|
||||||
} from '../types/user.types';
|
} from '../types/user.types';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
||||||
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
|
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
|
||||||
|
|
||||||
|
|
@ -298,4 +301,110 @@ export class UserService {
|
||||||
} : undefined
|
} : 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,3 +83,13 @@ export interface updateAvatarResponse {
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface SendVerifyEmailResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface VerifyEmailResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue