Commit 3d5a77c8 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

feat(challenge_10): add cron send email

parent 2db61caf
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@fast-csv/format": "^5.0.7", "@fast-csv/format": "^5.0.7",
"@types/node-cron": "^3.0.11",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"csv-parser": "^3.2.1", "csv-parser": "^3.2.1",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"module-alias": "^2.3.4", "module-alias": "^2.3.4",
"multer": "^2.1.1", "multer": "^2.1.1",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.7", "nodemailer": "^8.0.7",
"pg": "^8.20.0", "pg": "^8.20.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
......
...@@ -11,6 +11,9 @@ importers: ...@@ -11,6 +11,9 @@ importers:
'@fast-csv/format': '@fast-csv/format':
specifier: ^5.0.7 specifier: ^5.0.7
version: 5.0.7 version: 5.0.7
'@types/node-cron':
specifier: ^3.0.11
version: 3.0.11
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
...@@ -35,6 +38,9 @@ importers: ...@@ -35,6 +38,9 @@ importers:
multer: multer:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
node-cron:
specifier: ^4.2.1
version: 4.2.1
nodemailer: nodemailer:
specifier: ^8.0.7 specifier: ^8.0.7
version: 8.0.7 version: 8.0.7
...@@ -332,6 +338,9 @@ packages: ...@@ -332,6 +338,9 @@ packages:
'@types/multer@2.1.0': '@types/multer@2.1.0':
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==} resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
'@types/node-cron@3.0.11':
resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==}
'@types/node@25.7.0': '@types/node@25.7.0':
resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==}
...@@ -765,6 +774,10 @@ packages: ...@@ -765,6 +774,10 @@ packages:
resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==}
engines: {node: ^18 || ^20 || >= 21} engines: {node: ^18 || ^20 || >= 21}
node-cron@4.2.1:
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
engines: {node: '>=6.0.0'}
node-gyp-build@4.8.4: node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true hasBin: true
...@@ -1297,6 +1310,8 @@ snapshots: ...@@ -1297,6 +1310,8 @@ snapshots:
dependencies: dependencies:
'@types/express': 5.0.6 '@types/express': 5.0.6
'@types/node-cron@3.0.11': {}
'@types/node@25.7.0': '@types/node@25.7.0':
dependencies: dependencies:
undici-types: 7.21.0 undici-types: 7.21.0
...@@ -1739,6 +1754,8 @@ snapshots: ...@@ -1739,6 +1754,8 @@ snapshots:
node-addon-api@8.7.0: {} node-addon-api@8.7.0: {}
node-cron@4.2.1: {}
node-gyp-build@4.8.4: {} node-gyp-build@4.8.4: {}
nodemailer@8.0.7: {} nodemailer@8.0.7: {}
......
...@@ -5,6 +5,7 @@ import swaggerUi from 'swagger-ui-express'; ...@@ -5,6 +5,7 @@ import swaggerUi from 'swagger-ui-express';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import swaggerFile from '#docs/swagger/swagger-output.json'; import swaggerFile from '#docs/swagger/swagger-output.json';
import response from '#middlewares/response'; import response from '#middlewares/response';
import emailCronJob from '#services/schedulerService.js';
dotenv.config(); dotenv.config();
...@@ -20,6 +21,8 @@ _autoroutes(app, { ...@@ -20,6 +21,8 @@ _autoroutes(app, {
log: true log: true
}); });
emailCronJob.start();
app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerFile)); app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerFile));
app.listen(port, () => { app.listen(port, () => {
......
...@@ -56,4 +56,22 @@ export class EnrollProvider { ...@@ -56,4 +56,22 @@ export class EnrollProvider {
return users; return users;
} }
async getAllUserInEnrollment() {
const activeUsers = await models.users.findAll({
attributes: ["id", "name", "email"],
include: [
{
model: models.enrollments,
as: "enrollments",
where: { status: "active" },
attributes: [],
required: true
}
],
group: ['users.id']
});
return activeUsers;
}
} }
\ No newline at end of file
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
export class MailService { export class MailService {
private transporter = nodemailer.createTransport({ private instanceTransporter: nodemailer.Transporter | null = null;
service: 'gmail',
auth: { private getTransporter() {
user: process.env.EMAIL_USER, if (this.instanceTransporter) {
pass: process.env.EMAIL_PASS return this.instanceTransporter;
} }
});
this.instanceTransporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
}
});
return this.instanceTransporter;
}
async sendOtpEmail(toEmail: string, otp: string) { async sendOtpEmail(toEmail: string, otp: string) {
try { try {
...@@ -31,7 +41,7 @@ export class MailService { ...@@ -31,7 +41,7 @@ export class MailService {
`, `,
}; };
await this.transporter.sendMail(mailOptions); await this.getTransporter().sendMail(mailOptions);
return { return {
success: true, success: true,
...@@ -42,4 +52,31 @@ export class MailService { ...@@ -42,4 +52,31 @@ export class MailService {
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
} }
}; };
async sendEmailReminder(toEmail: string) {
try {
const mailOptions = {
from: `"Hệ thống thông báo" <${process.env.EMAIL_USER}>`,
to: toEmail,
subject: 'Nhắc nhở hàng ngày',
html: `
<div style="font-family: sans-serif; line-height: 1.5; color: #333;">
<h2>Thông báo tham gia lớp học</h2>
<p>Xin chào,</p>
<p>Đây là thông báo nhắc nhở bạn tham gia lớp học vào lúc 8:00 sáng hôm nay.</p>
</div>
`
};
await this.getTransporter().sendMail(mailOptions);
return {
success: true,
};
} catch (error) {
console.error('Lỗi gửi mail:', error);
return { success: false, error: (error as Error).message };
}
}
} }
\ No newline at end of file
import cron from 'node-cron';
import { MailService } from './mailService';
import { EnrollProvider } from '#providers/EnrollProvider.js';
const mailService = new MailService();
const enrollProvider = new EnrollProvider();
// gửi email nhắc nhở vào lúc 7 giờ sáng hàng ngày
const emailCronJob = cron.schedule('* 7 * * *', async () => {
try {
const getUser = await enrollProvider.getAllUserInEnrollment();
await Promise.all(getUser.map(async (user: any) => {
await mailService.sendEmailReminder(user.email);
console.log(`Email reminder sent to ${user.email}`);
}));
} catch (error) {
console.error('Error in email cron job:', error);
}
});
export default emailCronJob;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment