Skip to content

Commit a94b523

Browse files
authored
fix: update OTP email template ID and add new template (#283)
* fix: update OTP email template ID and add new template * chore: add dependency installation step to CI workflows * fix: update template validation and improve error handling in mail consumer * feat: add OTP email template for verification code * fix: improve error handling and logging in template rendering and mail sending * fix: update release workflow and improve template handling in mail consumer * fix: enhance templateId documentation for WhatsApp configuration and validation * fix: improve user fetching and error handling in OTP email sending * fix: enhance OTP email body generation with template rendering and structured error handling * fix: enhance validation for required fields and improve email format handling * fix: enhance script execution context and handle asynchronous results in validation scripts * fix: enhance OTP request validation and error handling for unregistered email and phone numbers * fix: improve error handling and user feedback for OTP requests in login templates * refactor: reverte modificações nos arquivos release, scripts e validateAndProcessValueFor * fix: update error messages for OTP verification to provide clearer user guidance * fix: refine error messages for OTP verification to enhance user clarity and guidance
1 parent 648e618 commit a94b523

File tree

12 files changed

+286
-57
lines changed

12 files changed

+286
-57
lines changed

.github/workflows/develop.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ jobs:
6161
node_modules
6262
.husky
6363
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
64+
- name: Install Dependencies
65+
run: yarn --frozen-lockfile --no-progress --non-interactive
66+
if: steps.cache-restore-yarn.outputs.cache-hit != 'true'
6467
- name: Restore Tests Cache
6568
uses: actions/cache/restore@v4
6669
id: cache-restore-jest

.github/workflows/merge-requests.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ jobs:
6464
node_modules
6565
.husky
6666
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
67+
- name: Install Dependencies
68+
run: yarn --frozen-lockfile --no-progress --non-interactive
69+
if: steps.cache-restore-yarn.outputs.cache-hit != 'true'
6770
- name: Restore Tests Cache
6871
uses: actions/cache/restore@v4
6972
id: cache-restore-jest

__test__/auth/otp/delivery.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('OTP Delivery Service', () => {
4040
(MetaObject.Namespace as any) = {
4141
otpConfig: {
4242
expirationMinutes: 5,
43-
emailTemplateId: 'email/otp.html',
43+
emailTemplateId: 'email/otp.hbs',
4444
emailFrom: 'test@example.com',
4545
whatsapp: {
4646
accessToken: 'test-token',
@@ -100,7 +100,7 @@ describe('OTP Delivery Service', () => {
100100

101101
(MetaObject.Namespace as any).otpConfig = {
102102
expirationMinutes: 5,
103-
emailTemplateId: 'email/otp.html',
103+
emailTemplateId: 'email/otp.hbs',
104104
emailFrom: 'test@example.com',
105105
whatsapp: {
106106
accessToken: 'test-token',

src/imports/auth/otp/delivery.ts

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import get from 'lodash/get';
2-
import { randomId } from '@imports/utils/random';
32
import { MetaObject } from '@imports/model/MetaObject';
43
import { User } from '@imports/model/User';
5-
import { DataDocument } from '@imports/types/data';
64
import { logger } from '@imports/utils/logger';
75
import queueManager from '@imports/queue/QueueManager';
86
import { sendOtpViaWhatsApp, WhatsAppConfig } from './whatsapp';
97
import { OTP_DEFAULT_EXPIRATION_MINUTES } from '../../consts';
8+
import { create } from '@imports/data/data';
9+
import { renderTemplate } from '@imports/template';
10+
import { randomId } from '@imports/utils/random';
1011

1112
export interface DeliveryResult {
1213
success: boolean;
@@ -163,36 +164,73 @@ async function sendViaEmail(phoneNumber: string | undefined, otpCode: string, us
163164
}
164165

165166
const expirationMinutes = MetaObject.Namespace.otpConfig?.expirationMinutes ?? OTP_DEFAULT_EXPIRATION_MINUTES;
166-
const templateId = MetaObject.Namespace.otpConfig?.emailTemplateId ?? 'email/otp.html';
167+
const templateId = MetaObject.Namespace.otpConfig?.emailTemplateId ?? 'email/otp.hbs';
167168
const emailFrom = MetaObject.Namespace.otpConfig?.emailFrom ?? 'Konecty <support@konecty.com>';
168169

169-
// Fetch user name for email template
170-
const user = (await MetaObject.Collections.User.findOne({ _id: userId }, { projection: { name: 1 } })) as Pick<User, 'name'> | null;
170+
// Fetch user for contextUser and email template
171+
const user = (await MetaObject.Collections.User.findOne({ _id: userId }, { projection: { _id: 1, name: 1 } })) as Pick<User, '_id' | 'name'> | null;
172+
173+
if (user == null) {
174+
return {
175+
success: false,
176+
error: 'User not found',
177+
};
178+
}
179+
180+
// Prepare template data
181+
const templateData = {
182+
otpCode,
183+
...(phoneNumber != null && { phoneNumber }),
184+
...(emailAddress != null && { email: emailAddress }),
185+
expirationMinutes,
186+
expiresAt: expiresAt.toISOString(),
187+
name: user.name,
188+
};
189+
190+
// Process template to generate body before creating message
191+
// This is required because email-service doesn't process templates
192+
let emailBody: string;
193+
const emailSubject = '[Konecty] Código de Verificação OTP';
194+
195+
try {
196+
const messageId = randomId();
197+
emailBody = await renderTemplate(templateId, { message: { _id: messageId }, ...templateData });
198+
} catch (error) {
199+
logger.error({ template: templateId, error: (error as Error).message }, 'Error rendering OTP email template');
200+
return {
201+
success: false,
202+
error: `Failed to render email template: ${(error as Error).message}`,
203+
};
204+
}
171205

172206
const messageData = {
173-
_id: randomId(),
174207
from: emailFrom,
175208
to: emailAddress,
176-
subject: '[Konecty] Código de Verificação OTP',
209+
subject: emailSubject,
177210
type: 'Email',
178211
status: 'Send',
179-
template: templateId,
212+
body: emailBody,
180213
discard: true,
181-
_createdAt: new Date(),
182-
_updatedAt: new Date(),
183-
data: {
184-
otpCode,
185-
...(phoneNumber != null && { phoneNumber }),
186-
...(emailAddress != null && { email: emailAddress }),
187-
expirationMinutes,
188-
expiresAt: expiresAt.toISOString(),
189-
name: user?.name,
190-
},
214+
_user: [{ _id: userId }],
215+
data: templateData,
191216
};
192217

193218
try {
194-
await MetaObject.Collections.Message.insertOne(messageData as DataDocument);
195-
return { success: true, method: 'email' };
219+
// Use data.create() instead of insertOne() to trigger events
220+
const result = await create({
221+
document: 'Message',
222+
data: messageData,
223+
contextUser: user as User,
224+
} as any);
225+
226+
if (result.success) {
227+
return { success: true, method: 'email' };
228+
}
229+
230+
return {
231+
success: false,
232+
error: Array.isArray(result.errors) ? result.errors.join(', ') : 'Failed to create message',
233+
};
196234
} catch (error) {
197235
logger.error(error, 'Error sending OTP via email');
198236
return {

src/imports/auth/otp/whatsapp.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export interface WhatsAppConfig {
66
accessToken: string;
77
phoneNumberId: string;
88
businessAccountId?: string;
9+
/**
10+
* Template name (not the numeric ID).
11+
* This should be the approved template name from Meta Business Manager (e.g., "hello_world", "otp_verification").
12+
* Do NOT use the numeric template ID (e.g., "751059698056406").
13+
*/
914
templateId: string;
1015
apiUrlTemplate?: string;
1116
languageCode?: string;
@@ -82,6 +87,8 @@ export async function sendOtpViaWhatsApp(phoneNumber: string, otpCode: string, c
8287
to: phoneNumber,
8388
type: 'template',
8489
template: {
90+
// Note: templateId should be the template NAME (not numeric ID)
91+
// e.g., "otp_verification" not "751059698056406"
8592
name: whatsappConfig.templateId,
8693
language: {
8794
code: languageCode,

src/imports/mailConsumer/index.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,11 @@ async function send(record) {
151151
return sendEmail(record);
152152
}
153153

154-
if (/.+\.hbs$/.test(record.template) === true) {
155-
set(record, 'template', `email/${record.template}`);
154+
if (/.+\.(hbs|html)$/.test(record.template) === true) {
155+
// Only add 'email/' prefix if template doesn't already start with it
156+
if (!record.template.startsWith('email/')) {
157+
set(record, 'template', `email/${record.template}`);
158+
}
156159
} else {
157160
const templateRecord = await MetaObject.Collections['Template'].findOne({ _id: record.template }, { projection: { subject: 1 } });
158161

@@ -168,7 +171,13 @@ async function send(record) {
168171
record.subject = Mustache.render(templateRecord.subject, record.data);
169172
}
170173

171-
record.body = await renderTemplate(record.template, extend({ message: { _id: record._id } }, record.data));
174+
try {
175+
record.body = await renderTemplate(record.template, extend({ message: { _id: record._id } }, record.data));
176+
} catch (error) {
177+
logger.error({ template: record.template, error: error.message }, 'Error rendering template');
178+
await MetaObject.Collections['Message'].updateOne({ _id: record._id }, { $set: { status: 'Falha no Envio', error: { message: error.message } } });
179+
return errorReturn(error.message);
180+
}
172181

173182
await MetaObject.Collections['Message'].updateOne({ _id: record._id }, { $set: { body: record.body, subject: record.subject } });
174183
return sendEmail(record);
@@ -217,7 +226,7 @@ async function consume() {
217226
} catch (error) {
218227
logger.error(error, `📧 Email error ${JSON.stringify(query, null, 2)}`);
219228

220-
return errorReturn("message" in error ? error.message : "Unknown error");
229+
return errorReturn('message' in error ? error.message : 'Unknown error');
221230
}
222231
});
223232

src/imports/meta/validateAndProcessValueFor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,13 +528,13 @@ export async function validateAndProcessValueFor({ meta, fieldName, value, actio
528528
if (emailObjectResult.success === false) {
529529
return emailObjectResult;
530530
}
531+
531532
const addressResult = mustBeString(value.address, `${fieldName}.address`);
532533
if (addressResult.success === false) {
533534
return addressResult;
534535
}
535536

536537
const typeResult = mustBeStringOrNull(value.type, `${fieldName}.type`);
537-
538538
if (typeResult.success === false) {
539539
return typeResult;
540540
}

src/imports/model/Namespace/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export const NamespaceSchema = z
5353
accessToken: z.string(),
5454
phoneNumberId: z.string(),
5555
businessAccountId: z.string().optional(),
56+
/**
57+
* Template name (not the numeric ID).
58+
* This should be the approved template name from Meta Business Manager.
59+
* Example: "otp_verification" (not "751059698056406")
60+
*/
5661
templateId: z.string(),
5762
apiUrlTemplate: z.string().optional(),
5863
languageCode: z.string().optional(),

src/imports/template/index.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,21 @@ export async function renderTemplate(templateId, data) {
2020
return DateTime.fromISO(str).toFormat(format);
2121
});
2222

23-
if (/.+\.hbs$/.test(templateId) === true) {
24-
const templateFullPath = path.join(templatePath(), templateId);
25-
const templateContent = await readFile(templateFullPath, 'utf-8');
26-
const template = LocalHandlebars.compile(templateContent);
27-
return template(data);
23+
if (/.+\.(hbs|html)$/.test(templateId) === true) {
24+
const basePath = templatePath();
25+
const templateFullPath = path.join(basePath, templateId);
26+
logger.debug({ templateId, basePath, templateFullPath }, 'Loading template file');
27+
try {
28+
const templateContent = await readFile(templateFullPath, 'utf-8');
29+
const template = LocalHandlebars.compile(templateContent);
30+
return template(data);
31+
} catch (error) {
32+
if (error.code === 'ENOENT') {
33+
logger.error({ templateId, basePath, templateFullPath, cwd: process.cwd(), nodeEnv: process.env.NODE_ENV }, `Template file not found: ${templateId}`);
34+
throw new Error(`Template ${templateId} not found`);
35+
}
36+
throw error;
37+
}
2838
}
2939

3040
const templateCollection = MetaObject.Collections['Template'];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div style="text-align: center">
2+
Olá {{name}}, seu código de verificação OTP foi gerado.<br /><br />
3+
<div style="margin: 20px">
4+
<span style="background-color: #eee; border-radius: 10px; padding: 10px 20px; font-size: 24px; font-weight: bold"> {{otpCode}} </span>
5+
</div>
6+
<br />
7+
Este código expira em {{expirationMinutes}} minutos ({{expiresAt}}).<br />
8+
<br />
9+
Se você não solicitou este código, ignore este e-mail.
10+
</div>
11+

0 commit comments

Comments
 (0)