diff --git a/scripts/patch-forgot-password-template.sql b/scripts/patch-forgot-password-template.sql new file mode 100644 index 0000000..41651e2 --- /dev/null +++ b/scripts/patch-forgot-password-template.sql @@ -0,0 +1,46 @@ +-- Forgot password email template (additive; safe if full patch already applied) + +INSERT INTO notification_templates ( + code, + name, + channel, + subject, + html_body, + placeholders, + description, + is_active +) +VALUES ( + 'FORGOT_PASSWORD', + 'Forgot Password Reset', + 'EMAIL', + 'Reset your ERP password', + ' + +
+Hello {{user_name}},
+We received a request to reset your password.
+Click here to reset your password
+This link expires in {{expiry_minutes}} minutes.
+If the button does not work, open this link:
{{reset_url}}
If you did not request this, you can ignore this email.
+ +', + '[ + {"key": "user_name", "description": "User full name"}, + {"key": "reset_url", "description": "Password reset frontend URL with token"}, + {"key": "expiry_minutes", "description": "Reset link expiry in minutes"} + ]'::jsonb, + 'Sent when a user requests a password reset via forgot-password', + true +) +ON CONFLICT (code) DO UPDATE SET + name = EXCLUDED.name, + channel = EXCLUDED.channel, + subject = EXCLUDED.subject, + html_body = EXCLUDED.html_body, + placeholders = EXCLUDED.placeholders, + description = EXCLUDED.description, + is_active = true, + deleted_at = NULL, + updated_at = NOW(); diff --git a/scripts/patch-notification-templates.sql b/scripts/patch-notification-templates.sql index 4655ef6..22f5920 100644 --- a/scripts/patch-notification-templates.sql +++ b/scripts/patch-notification-templates.sql @@ -93,3 +93,48 @@ ON CONFLICT (code) DO UPDATE SET is_active = true, deleted_at = NULL, updated_at = NOW(); + +INSERT INTO notification_templates ( + code, + name, + channel, + subject, + html_body, + placeholders, + description, + is_active +) +VALUES ( + 'FORGOT_PASSWORD', + 'Forgot Password Reset', + 'EMAIL', + 'Reset your ERP password', + ' + + +Hello {{user_name}},
+We received a request to reset your password.
+Click here to reset your password
+This link expires in {{expiry_minutes}} minutes.
+If the button does not work, open this link:
{{reset_url}}
If you did not request this, you can ignore this email.
+ +', + '[ + {"key": "user_name", "description": "User full name"}, + {"key": "reset_url", "description": "Password reset frontend URL with token"}, + {"key": "expiry_minutes", "description": "Reset link expiry in minutes"} + ]'::jsonb, + 'Sent when a user requests a password reset via forgot-password', + true +) +ON CONFLICT (code) DO UPDATE SET + name = EXCLUDED.name, + channel = EXCLUDED.channel, + subject = EXCLUDED.subject, + html_body = EXCLUDED.html_body, + placeholders = EXCLUDED.placeholders, + description = EXCLUDED.description, + is_active = true, + deleted_at = NULL, + updated_at = NOW(); diff --git a/src/modules/auth/auth.service.js b/src/modules/auth/auth.service.js index 9ccb98d..349c523 100644 --- a/src/modules/auth/auth.service.js +++ b/src/modules/auth/auth.service.js @@ -9,7 +9,7 @@ const auditLog = require('../../utils/auditLog'); const env = require('../../config/env'); const { encrypt, decrypt, blindIndex } = require('../../utils/encryption'); const { collectUserPermissions, extractUserRoles } = require('../../utils/userPermissions'); -const { sendMail } = require('../../utils/email'); +const { sendTemplatedEmail, TEMPLATE_CODES } = require('../notifications/notifications.service'); const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex'); @@ -344,30 +344,16 @@ const forgotPassword = async (email) => { }); const resetUrl = `${env.FRONTEND_URL.replace(/\/$/, '')}/reset-password?token=${rawToken}`; - const subject = 'Reset your ERP password'; - const text = [ - `Hello ${user.full_name},`, - '', - 'We received a request to reset your password.', - `Open this link to set a new password (valid for ${env.PASSWORD_RESET_EXPIRY_MINUTES} minutes):`, - resetUrl, - '', - 'If you did not request this, you can ignore this email.', - ].join('\n'); - const html = ` -Hello ${user.full_name},
-We received a request to reset your password.
-Click here to reset your password
-This link expires in ${env.PASSWORD_RESET_EXPIRY_MINUTES} minutes.
-If you did not request this, you can ignore this email.
- `; - - await sendMail({ + await sendTemplatedEmail({ + templateCode: TEMPLATE_CODES.FORGOT_PASSWORD, to: user.email, - subject, - text, - html, + data: { + user_name: user.full_name || 'User', + reset_url: resetUrl, + expiry_minutes: env.PASSWORD_RESET_EXPIRY_MINUTES, + }, + context: { userId: String(user.id) }, }); return { message: genericMessage }; diff --git a/src/modules/notifications/notifications.constants.js b/src/modules/notifications/notifications.constants.js index 14ef33c..6e0b942 100644 --- a/src/modules/notifications/notifications.constants.js +++ b/src/modules/notifications/notifications.constants.js @@ -1,5 +1,6 @@ const TEMPLATE_CODES = { PO_SUBMIT_APPROVAL: 'PO_SUBMIT_APPROVAL', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', }; const CHANNELS = { diff --git a/src/modules/notifications/notifications.validation.js b/src/modules/notifications/notifications.validation.js index 66579aa..8d38af8 100644 --- a/src/modules/notifications/notifications.validation.js +++ b/src/modules/notifications/notifications.validation.js @@ -1,13 +1,17 @@ const Joi = require('joi'); const { TEMPLATE_CODES } = require('./notifications.constants'); +/** Templates that can be triggered from the FE button API */ +const TRIGGERABLE_TEMPLATE_CODES = [TEMPLATE_CODES.PO_SUBMIT_APPROVAL]; + const triggerNotificationSchema = Joi.object({ template_code: Joi.string() - .valid(...Object.values(TEMPLATE_CODES)) + .valid(...TRIGGERABLE_TEMPLATE_CODES) .required(), po_id: Joi.alternatives().try(Joi.number().integer().positive(), Joi.string().pattern(/^\d+$/)).required(), }); module.exports = { triggerNotificationSchema, + TRIGGERABLE_TEMPLATE_CODES, };