@venizia/ignis-docs 0.0.5 → 0.0.6-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/wiki/best-practices/architecture-decisions.md +0 -8
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/components.md +2 -2
- package/wiki/guides/core-concepts/dependency-injection.md +1 -1
- package/wiki/guides/core-concepts/services.md +1 -1
- package/wiki/guides/tutorials/building-a-crud-api.md +1 -1
- package/wiki/guides/tutorials/ecommerce-api.md +2 -2
- package/wiki/guides/tutorials/realtime-chat.md +6 -6
- package/wiki/guides/tutorials/testing.md +1 -1
- package/wiki/references/base/bootstrapping.md +0 -2
- package/wiki/references/base/components.md +2 -2
- package/wiki/references/base/controllers.md +0 -1
- package/wiki/references/base/datasources.md +1 -1
- package/wiki/references/base/dependency-injection.md +1 -1
- package/wiki/references/base/filter-system/quick-reference.md +0 -14
- package/wiki/references/base/middlewares.md +0 -8
- package/wiki/references/base/providers.md +0 -9
- package/wiki/references/base/services.md +0 -1
- package/wiki/references/components/authentication/api.md +444 -0
- package/wiki/references/components/authentication/errors.md +177 -0
- package/wiki/references/components/authentication/index.md +571 -0
- package/wiki/references/components/authentication/usage.md +781 -0
- package/wiki/references/components/health-check.md +292 -103
- package/wiki/references/components/index.md +14 -12
- package/wiki/references/components/mail/api.md +505 -0
- package/wiki/references/components/mail/errors.md +176 -0
- package/wiki/references/components/mail/index.md +535 -0
- package/wiki/references/components/mail/usage.md +404 -0
- package/wiki/references/components/request-tracker.md +229 -25
- package/wiki/references/components/socket-io/api.md +1051 -0
- package/wiki/references/components/socket-io/errors.md +119 -0
- package/wiki/references/components/socket-io/index.md +410 -0
- package/wiki/references/components/socket-io/usage.md +322 -0
- package/wiki/references/components/static-asset/api.md +261 -0
- package/wiki/references/components/static-asset/errors.md +89 -0
- package/wiki/references/components/static-asset/index.md +617 -0
- package/wiki/references/components/static-asset/usage.md +364 -0
- package/wiki/references/components/swagger.md +390 -110
- package/wiki/references/components/template/api-page.md +125 -0
- package/wiki/references/components/template/errors-page.md +100 -0
- package/wiki/references/components/template/index.md +104 -0
- package/wiki/references/components/template/setup-page.md +134 -0
- package/wiki/references/components/template/single-page.md +132 -0
- package/wiki/references/components/template/usage-page.md +127 -0
- package/wiki/references/components/websocket/api.md +508 -0
- package/wiki/references/components/websocket/errors.md +123 -0
- package/wiki/references/components/websocket/index.md +453 -0
- package/wiki/references/components/websocket/usage.md +475 -0
- package/wiki/references/helpers/cron/index.md +224 -0
- package/wiki/references/helpers/crypto/index.md +537 -0
- package/wiki/references/helpers/env/index.md +214 -0
- package/wiki/references/helpers/error/index.md +232 -0
- package/wiki/references/helpers/index.md +16 -15
- package/wiki/references/helpers/inversion/index.md +608 -0
- package/wiki/references/helpers/logger/index.md +600 -0
- package/wiki/references/helpers/network/api.md +986 -0
- package/wiki/references/helpers/network/index.md +620 -0
- package/wiki/references/helpers/queue/index.md +589 -0
- package/wiki/references/helpers/redis/index.md +495 -0
- package/wiki/references/helpers/socket-io/api.md +497 -0
- package/wiki/references/helpers/socket-io/index.md +513 -0
- package/wiki/references/helpers/storage/api.md +705 -0
- package/wiki/references/helpers/storage/index.md +583 -0
- package/wiki/references/helpers/template/index.md +66 -0
- package/wiki/references/helpers/template/single-page.md +126 -0
- package/wiki/references/helpers/testing/index.md +510 -0
- package/wiki/references/helpers/types/index.md +512 -0
- package/wiki/references/helpers/uid/index.md +272 -0
- package/wiki/references/helpers/websocket/api.md +736 -0
- package/wiki/references/helpers/websocket/index.md +574 -0
- package/wiki/references/helpers/worker-thread/index.md +470 -0
- package/wiki/references/quick-reference.md +3 -18
- package/wiki/references/utilities/jsx.md +1 -8
- package/wiki/references/utilities/statuses.md +0 -7
- package/wiki/references/components/authentication.md +0 -476
- package/wiki/references/components/mail.md +0 -687
- package/wiki/references/components/socket-io.md +0 -562
- package/wiki/references/components/static-asset.md +0 -1277
- package/wiki/references/helpers/cron.md +0 -108
- package/wiki/references/helpers/crypto.md +0 -132
- package/wiki/references/helpers/env.md +0 -83
- package/wiki/references/helpers/error.md +0 -97
- package/wiki/references/helpers/inversion.md +0 -176
- package/wiki/references/helpers/logger.md +0 -296
- package/wiki/references/helpers/network.md +0 -396
- package/wiki/references/helpers/queue.md +0 -150
- package/wiki/references/helpers/redis.md +0 -142
- package/wiki/references/helpers/socket-io.md +0 -932
- package/wiki/references/helpers/storage.md +0 -665
- package/wiki/references/helpers/testing.md +0 -133
- package/wiki/references/helpers/types.md +0 -167
- package/wiki/references/helpers/uid.md +0 -167
- package/wiki/references/helpers/worker-thread.md +0 -178
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Mail -- Usage & Examples
|
|
2
|
+
|
|
3
|
+
> Practical examples for sending emails, using templates, queue executors, verification generators, and batch operations.
|
|
4
|
+
|
|
5
|
+
## Sending Emails
|
|
6
|
+
|
|
7
|
+
Inject `IMailService` via the `MailKeys.MAIL_SERVICE` binding key to send emails from any service.
|
|
8
|
+
|
|
9
|
+
**Sending a simple email:**
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { BaseService, inject } from '@venizia/ignis';
|
|
13
|
+
import { MailKeys, type IMailService } from '@venizia/ignis/mail';
|
|
14
|
+
|
|
15
|
+
export class UserService extends BaseService {
|
|
16
|
+
constructor(
|
|
17
|
+
@inject({ key: MailKeys.MAIL_SERVICE })
|
|
18
|
+
private _mailService: IMailService,
|
|
19
|
+
) {
|
|
20
|
+
super({ scope: UserService.name });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async sendWelcomeEmail(opts: { userEmail: string; userName: string }) {
|
|
24
|
+
const result = await this._mailService.send({
|
|
25
|
+
to: opts.userEmail,
|
|
26
|
+
subject: 'Welcome to Our App!',
|
|
27
|
+
html: `<h1>Welcome ${opts.userName}!</h1><p>Thanks for joining us.</p>`,
|
|
28
|
+
text: `Welcome ${opts.userName}! Thanks for joining us.`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.success) {
|
|
32
|
+
this.logger.info('[sendWelcomeEmail] Email sent: %s', result.messageId);
|
|
33
|
+
} else {
|
|
34
|
+
this.logger.error('[sendWelcomeEmail] Failed to send email: %s', result.error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Batch email sending:**
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
async sendBulkNotifications(users: Array<{ email: string; name: string }>) {
|
|
46
|
+
const messages = users.map(user => ({
|
|
47
|
+
to: user.email,
|
|
48
|
+
subject: 'Important Update',
|
|
49
|
+
html: `<p>Hello ${user.name}, we have an important update for you.</p>`,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const results = await this.mailService.sendBatch(messages, {
|
|
53
|
+
concurrency: 5, // Send 5 emails at a time
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const successCount = results.filter(r => r.success).length;
|
|
57
|
+
this.logger.info(
|
|
58
|
+
'[sendBulkNotifications] Sent %d/%d emails successfully',
|
|
59
|
+
successCount,
|
|
60
|
+
results.length,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Message validation:**
|
|
68
|
+
|
|
69
|
+
The `MailService` validates every message before sending via its internal `validateMessage()` method. This pre-transport check throws immediately if any of these conditions is met:
|
|
70
|
+
|
|
71
|
+
| Condition | Error Code | Message |
|
|
72
|
+
|-----------|-----------|---------|
|
|
73
|
+
| `to` is missing or empty array | `MailErrorCodes.INVALID_RECIPIENT` | `Recipient email address is required` |
|
|
74
|
+
| `subject` is missing | `MailErrorCodes.INVALID_CONFIGURATION` | `Email subject is required` |
|
|
75
|
+
| Both `text` and `html` are missing | `MailErrorCodes.INVALID_CONFIGURATION` | `Email must have either text or html content` |
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// This will throw BEFORE reaching the transport
|
|
79
|
+
await mailService.send({
|
|
80
|
+
to: 'user@example.com',
|
|
81
|
+
subject: '', // Empty subject triggers validation error
|
|
82
|
+
html: '<p>Hello</p>',
|
|
83
|
+
});
|
|
84
|
+
// Error: { statusCode: 400, messageCode: 'MAIL_INVALID_CONFIGURATION', message: 'Email subject is required' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Template Engine
|
|
88
|
+
|
|
89
|
+
### Using Templates
|
|
90
|
+
|
|
91
|
+
Inject both `IMailTemplateEngine` and `IMailService` to register templates and send template-based emails.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { BaseService, inject } from '@venizia/ignis';
|
|
95
|
+
import { MailKeys, type IMailTemplateEngine, type IMailService } from '@venizia/ignis/mail';
|
|
96
|
+
|
|
97
|
+
export class NotificationService extends BaseService {
|
|
98
|
+
constructor(
|
|
99
|
+
@inject({ key: MailKeys.MAIL_TEMPLATE_ENGINE })
|
|
100
|
+
private templateEngine: IMailTemplateEngine,
|
|
101
|
+
@inject({ key: MailKeys.MAIL_SERVICE })
|
|
102
|
+
private mailService: IMailService,
|
|
103
|
+
) {
|
|
104
|
+
super({ scope: NotificationService.name });
|
|
105
|
+
this.registerTemplates();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
registerTemplates() {
|
|
109
|
+
// Register a welcome email template
|
|
110
|
+
this.templateEngine.registerTemplate({
|
|
111
|
+
name: 'welcome-email',
|
|
112
|
+
content: `
|
|
113
|
+
<html>
|
|
114
|
+
<body>
|
|
115
|
+
<h1>Welcome {{userName}}!</h1>
|
|
116
|
+
<p>Your account has been created successfully.</p>
|
|
117
|
+
<p>Your verification code is: <strong>{{verificationCode}}</strong></p>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
`,
|
|
121
|
+
options: {
|
|
122
|
+
subject: 'Welcome to {{appName}}',
|
|
123
|
+
description: 'Welcome email for new users',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async sendWelcomeEmail(userEmail: string, userName: string, verificationCode: string) {
|
|
129
|
+
const result = await this.mailService.sendTemplate({
|
|
130
|
+
templateName: 'welcome-email',
|
|
131
|
+
data: {
|
|
132
|
+
userName,
|
|
133
|
+
verificationCode,
|
|
134
|
+
appName: 'My Application',
|
|
135
|
+
},
|
|
136
|
+
recipients: userEmail,
|
|
137
|
+
options: {
|
|
138
|
+
// Optional: override template subject or add attachments
|
|
139
|
+
attachments: [
|
|
140
|
+
{
|
|
141
|
+
filename: 'logo.png',
|
|
142
|
+
path: '/path/to/logo.png',
|
|
143
|
+
cid: 'logo',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Template Rendering
|
|
155
|
+
|
|
156
|
+
The `TemplateEngineService` provides a simple <code v-pre>{{variable}}</code> substitution engine using an in-memory `Map<string, ITemplate>` as its template store.
|
|
157
|
+
|
|
158
|
+
The `renderSimpleTemplate()` method uses regex `/\{\{(\s*[\w.]+\s*)\}\}/g` to find placeholders. For each match:
|
|
159
|
+
|
|
160
|
+
1. The key is trimmed of whitespace
|
|
161
|
+
2. Nested value lookup via dot notation (e.g., `user.profile.name` resolves by splitting on `.` and walking the object)
|
|
162
|
+
3. If the value is `undefined` or `null`, the **original placeholder is preserved as-is** (e.g., <code v-pre>{{missingKey}}</code> remains literally in the output). A warning is logged
|
|
163
|
+
4. Otherwise, the value is converted to string via `String(value)`
|
|
164
|
+
|
|
165
|
+
> [!IMPORTANT]
|
|
166
|
+
> Missing template variables are **not** replaced with empty strings. The original <code v-pre>{{placeholder}}</code> text is preserved in the output. This makes debugging easier since you can see which variables were not resolved.
|
|
167
|
+
|
|
168
|
+
**Template Features:**
|
|
169
|
+
|
|
170
|
+
- Simple <code v-pre>{{variable}}</code> syntax (no loops or conditionals)
|
|
171
|
+
- Nested object access via dot notation: <code v-pre>{{user.profile.name}}</code>
|
|
172
|
+
- Subject line templating (subjects are rendered through the same engine)
|
|
173
|
+
- HTML and plain text support
|
|
174
|
+
- Validation before rendering (optional, throws on missing keys)
|
|
175
|
+
- In-memory template registry (`Map<string, ITemplate>`)
|
|
176
|
+
- Template metadata (subject, description via `ITemplate`)
|
|
177
|
+
- Missing placeholders preserved as-is (not replaced with empty strings)
|
|
178
|
+
- `clearTemplates()` to reset the entire registry
|
|
179
|
+
|
|
180
|
+
### Template Validation
|
|
181
|
+
|
|
182
|
+
`validateTemplateData()` extracts all unique placeholder keys from a template string and checks if each key resolves to a non-null, non-undefined value in the data object. It returns:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
{
|
|
186
|
+
isValid: boolean; // true if all placeholders have values
|
|
187
|
+
missingKeys: string[]; // placeholder names missing from data
|
|
188
|
+
allKeys: string[]; // all unique placeholder names found
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
When `requireValidate: true` is passed to `render()` or `renderSimpleTemplate()`, validation runs first and throws with `MailErrorCodes.INVALID_CONFIGURATION` if any keys are missing.
|
|
193
|
+
|
|
194
|
+
Template validation example:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
const template = '<h1>Hello {{userName}}, your code is {{code}}</h1>';
|
|
198
|
+
const data = { userName: 'John' }; // Missing 'code'
|
|
199
|
+
|
|
200
|
+
const validation = this.templateEngine.validateTemplateData({ template, data });
|
|
201
|
+
|
|
202
|
+
if (!validation.isValid) {
|
|
203
|
+
console.error('Missing template variables:', validation.missingKeys);
|
|
204
|
+
// Output: ['code']
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Render with validation
|
|
208
|
+
try {
|
|
209
|
+
const html = this.templateEngine.render({
|
|
210
|
+
templateData: template,
|
|
211
|
+
data,
|
|
212
|
+
requireValidate: true, // Throws error if validation fails
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Template rendering failed:', error.message);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Syncing Templates from a Database
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
async syncTemplatesFromDatabase() {
|
|
223
|
+
const templateEngine = this.application.get<IMailTemplateEngine>({
|
|
224
|
+
key: MailKeys.MAIL_TEMPLATE_ENGINE,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const configRepository = this.application.get<ConfigurationRepository>({
|
|
228
|
+
key: 'repositories.ConfigurationRepository',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const templateConfigs = await configRepository.find({
|
|
232
|
+
filter: {
|
|
233
|
+
where: {
|
|
234
|
+
code: { inq: ['MAIL_TEMPLATE_WELCOME', 'MAIL_TEMPLATE_VERIFICATION'] },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
templateConfigs.forEach(config => {
|
|
240
|
+
templateEngine.registerTemplate({
|
|
241
|
+
name: config.code,
|
|
242
|
+
content: config.jValue.content,
|
|
243
|
+
options: {
|
|
244
|
+
subject: config.jValue.subject,
|
|
245
|
+
description: config.jValue.description,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
this.logger.info('[syncTemplates] Registered template: %s', config.code);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Queue Executors
|
|
254
|
+
|
|
255
|
+
### Direct Executor
|
|
256
|
+
|
|
257
|
+
The simplest executor. `DirectMailExecutorHelper` extends `BaseHelper`. Calls the processor function immediately without any queueing. Returns `{ queued: false, ... }` to indicate no queue was used. Throws if `setProcessor()` has not been called. Useful for development environments or when you need guaranteed synchronous email sending.
|
|
258
|
+
|
|
259
|
+
### Internal Queue Executor
|
|
260
|
+
|
|
261
|
+
`InternalQueueMailExecutorHelper` extends `BaseHelper`. Uses the in-memory `QueueHelper` from `@venizia/ignis-helpers` with `autoDispatch: true`. Key behaviors:
|
|
262
|
+
|
|
263
|
+
- Generates job IDs in the format `job_<counter>_<timestamp>`
|
|
264
|
+
- Supports delayed jobs via `setTimeout` (stored in a `delayedJobs` Map)
|
|
265
|
+
- Retry logic: on failure, retries up to `options.attempts` (default 3) with configurable backoff
|
|
266
|
+
- Backoff calculation: `exponential` uses `delay * 2^(attempt-1)`, `fixed` uses the raw delay, no backoff config defaults to 1000ms
|
|
267
|
+
- Does not persist jobs across restarts
|
|
268
|
+
- Logs queue state changes and individual job lifecycle events
|
|
269
|
+
|
|
270
|
+
### BullMQ Executor
|
|
271
|
+
|
|
272
|
+
`BullMQMailExecutorHelper` extends `BaseHelper`. Full-featured Redis-backed queue with:
|
|
273
|
+
|
|
274
|
+
- Job persistence across restarts
|
|
275
|
+
- Distributed worker support
|
|
276
|
+
- Configurable retry strategies (exponential by default, with 1000ms base delay)
|
|
277
|
+
- Job prioritization
|
|
278
|
+
- Delayed job execution
|
|
279
|
+
- Job progress tracking via worker callbacks
|
|
280
|
+
- `removeOnComplete: true`, `removeOnFail: false` (failed jobs retained for debugging)
|
|
281
|
+
|
|
282
|
+
**Mode behavior:**
|
|
283
|
+
|
|
284
|
+
| Mode | Queue Initialized | Workers Created | Can Enqueue | Can Process |
|
|
285
|
+
|------|-------------------|-----------------|-------------|-------------|
|
|
286
|
+
| `'queue-only'` | Yes | No (skipped in `setProcessor`) | Yes | No |
|
|
287
|
+
| `'worker-only'` | No | Yes | No (throws) | Yes |
|
|
288
|
+
| `'both'` | Yes | Yes | Yes | Yes |
|
|
289
|
+
|
|
290
|
+
## Verification Generators
|
|
291
|
+
|
|
292
|
+
Three generators are registered by `MailComponent`:
|
|
293
|
+
|
|
294
|
+
- **`NumericCodeGenerator`** -- Implements `IVerificationCodeGenerator`. Generates numeric verification codes of configurable length (e.g., 6-digit `"482917"`)
|
|
295
|
+
- **`RandomTokenGenerator`** -- Implements `IVerificationTokenGenerator`. Generates cryptographically random **base64url**-encoded tokens of configurable byte length
|
|
296
|
+
- **`DefaultVerificationDataGenerator`** -- Implements `IVerificationDataGenerator`. Composes both generators via `@inject` and produces a full `IVerificationData` object with expiry timestamps
|
|
297
|
+
|
|
298
|
+
**NumericCodeGenerator:**
|
|
299
|
+
|
|
300
|
+
Generates cryptographically random numeric codes. Uses `crypto.randomInt(0, 10^length)` to ensure uniform distribution. The result is zero-padded to the requested length via `padStart()` (e.g., code `42` with length 6 becomes `"000042"`).
|
|
301
|
+
|
|
302
|
+
**RandomTokenGenerator:**
|
|
303
|
+
|
|
304
|
+
Generates URL-safe random tokens using `crypto.randomBytes(bytes).toString('base64url')`. The output is **base64url-encoded** (not hex). For 32 bytes of input, this produces a 43-character base64url string (not 64 hex characters). Base64url encoding uses characters `A-Z`, `a-z`, `0-9`, `-`, `_` with no padding.
|
|
305
|
+
|
|
306
|
+
**DefaultVerificationDataGenerator:**
|
|
307
|
+
|
|
308
|
+
Uses `@inject` to receive both `NumericCodeGenerator` (via `MailKeys.MAIL_VERIFICATION_CODE_GENERATOR`) and `RandomTokenGenerator` (via `MailKeys.MAIL_VERIFICATION_TOKEN_GENERATOR`). Produces a complete verification data object with:
|
|
309
|
+
- A short numeric code for manual entry (SMS, email)
|
|
310
|
+
- A long random base64url token for URL-based verification
|
|
311
|
+
- Separate expiry times: code uses `getExpiryTime(minutes)`, token uses `getExpiryTimeInHours(hours)`
|
|
312
|
+
- Generation timestamps in ISO 8601 format
|
|
313
|
+
- Attempt counter (set to 0 initially)
|
|
314
|
+
- `lastCodeSentAt` set to `now`
|
|
315
|
+
|
|
316
|
+
**Email verification flow example:**
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { BaseService, inject } from '@venizia/ignis';
|
|
320
|
+
import {
|
|
321
|
+
MailKeys,
|
|
322
|
+
type IMailService,
|
|
323
|
+
type IVerificationDataGenerator,
|
|
324
|
+
} from '@venizia/ignis/mail';
|
|
325
|
+
|
|
326
|
+
export class AuthService extends BaseService {
|
|
327
|
+
constructor(
|
|
328
|
+
@inject({ key: MailKeys.MAIL_SERVICE })
|
|
329
|
+
private mailService: IMailService,
|
|
330
|
+
@inject({ key: MailKeys.MAIL_VERIFICATION_DATA_GENERATOR })
|
|
331
|
+
private verificationGenerator: IVerificationDataGenerator,
|
|
332
|
+
) {
|
|
333
|
+
super({ scope: AuthService.name });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async sendVerificationEmail(userEmail: string) {
|
|
337
|
+
// Generate verification code and token
|
|
338
|
+
const verificationData = this.verificationGenerator.generateVerificationData({
|
|
339
|
+
codeLength: 6, // 6-digit code
|
|
340
|
+
tokenBytes: 32, // 32-byte token
|
|
341
|
+
codeExpiryMinutes: 10, // Code expires in 10 minutes
|
|
342
|
+
tokenExpiryHours: 24, // Token expires in 24 hours
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Save verification data to database
|
|
346
|
+
// await this.saveVerificationData(userEmail, verificationData);
|
|
347
|
+
|
|
348
|
+
// Send verification email
|
|
349
|
+
const result = await this.mailService.send({
|
|
350
|
+
to: userEmail,
|
|
351
|
+
subject: 'Email Verification',
|
|
352
|
+
html: `
|
|
353
|
+
<h2>Verify Your Email</h2>
|
|
354
|
+
<p>Your verification code is: <strong>${verificationData.verificationCode}</strong></p>
|
|
355
|
+
<p>This code expires at: ${verificationData.codeExpiresAt}</p>
|
|
356
|
+
<p>Or click this link: https://example.com/verify?token=${verificationData.verificationToken}</p>
|
|
357
|
+
`,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return { result, verificationData };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Storing verification data:**
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
const verificationData = this.verificationGenerator.generateVerificationData({
|
|
369
|
+
codeLength: 6, // 6-digit code
|
|
370
|
+
tokenBytes: 32, // 32-byte token -> 43-char base64url string
|
|
371
|
+
codeExpiryMinutes: 10, // Code expires in 10 minutes
|
|
372
|
+
tokenExpiryHours: 24, // Token expires in 24 hours
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Store in database
|
|
376
|
+
await this.userRepo.update({
|
|
377
|
+
where: { id: userId },
|
|
378
|
+
data: {
|
|
379
|
+
verificationCode: verificationData.verificationCode,
|
|
380
|
+
verificationCodeExpiresAt: new Date(verificationData.codeExpiresAt),
|
|
381
|
+
verificationToken: verificationData.verificationToken,
|
|
382
|
+
verificationTokenExpiresAt: new Date(verificationData.tokenExpiresAt),
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Security Note
|
|
388
|
+
|
|
389
|
+
The `MailComponent.createAndBindInstances()` method logs the full `mailOptions` object at `info` level:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
this.logger.for(this.createAndBindInstances.name).info('Mail Options: %j', mailOptions);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
This includes sensitive fields such as SMTP passwords, OAuth2 client secrets, refresh tokens, and API keys. Similarly, the queue executor config (which may contain Redis passwords) is logged. In production environments, ensure your logging configuration either:
|
|
396
|
+
- Sets the mail component scope to a level higher than `info`
|
|
397
|
+
- Uses a log pipeline that redacts sensitive fields
|
|
398
|
+
- Strips credential fields before binding the options
|
|
399
|
+
|
|
400
|
+
## See Also
|
|
401
|
+
|
|
402
|
+
- [Setup & Configuration](./) -- Quick reference, setup steps, configuration options, and binding keys
|
|
403
|
+
- [API Reference](./api) -- Architecture, interfaces, and internals
|
|
404
|
+
- [Error Reference](./errors) -- Error codes and troubleshooting
|
|
@@ -1,50 +1,254 @@
|
|
|
1
|
-
# Request Tracker
|
|
1
|
+
# Request Tracker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automatic request logging middleware with unique request IDs, client IP detection, body parsing, and timing -- auto-registered by the framework.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> This component is **auto-registered** by `BaseApplication` during `initialize()`. No manual registration is needed.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- **Purpose:** To log incoming requests and add a unique request ID for tracing purposes.
|
|
9
|
-
- **Background:** In a production environment, it is crucial to have detailed logs for debugging and monitoring. The Request Tracker component provides a way to automatically log every incoming request with a unique ID, making it easier to trace the entire lifecycle of a request.
|
|
10
|
-
- **Related Features/Modules:** This component is a default middleware that is registered at the application level and integrates with the core logging feature.
|
|
8
|
+
## Quick Reference
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
| Item | Value |
|
|
11
|
+
|------|-------|
|
|
12
|
+
| **Package** | `@venizia/ignis` |
|
|
13
|
+
| **Component** | `RequestTrackerComponent` |
|
|
14
|
+
| **Middleware** | `RequestSpyMiddleware` |
|
|
15
|
+
| **Utility** | `getIncomingIp()` |
|
|
16
|
+
| **Runtimes** | Both (Bun and Node.js) |
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
#### Import Paths
|
|
19
|
+
```typescript
|
|
20
|
+
import { RequestTrackerComponent } from '@venizia/ignis';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
### Step 1: Bind Configuration
|
|
26
|
+
|
|
27
|
+
No configuration binding is required. The component has no user-facing configuration options.
|
|
28
|
+
|
|
29
|
+
### Step 2: Register Component
|
|
16
30
|
|
|
17
|
-
|
|
31
|
+
This happens automatically inside `BaseApplication.initialize()`:
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
```typescript
|
|
34
|
+
// Internal to BaseApplication -- shown for reference only
|
|
35
|
+
this.component(RequestTrackerComponent);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The component registers two Hono middlewares on the application server during its `binding()` phase:
|
|
39
|
+
1. `requestId()` from `hono/request-id` -- generates a UUID and stores it on the Hono context under the key `'requestId'`
|
|
40
|
+
2. `RequestSpyMiddleware` -- logs request start/end with IP, method, path, query, body, and timing
|
|
41
|
+
|
|
42
|
+
### Step 3: Use
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
- **`hono/request-id`**
|
|
44
|
+
No injection or manual usage is needed. Once the application starts, every incoming request is automatically logged with a unique request ID.
|
|
23
45
|
|
|
24
|
-
|
|
46
|
+
A sample log output in **non-production** mode looks like this:
|
|
25
47
|
|
|
26
|
-
|
|
48
|
+
```
|
|
49
|
+
[SpyMW] [<request-id>][127.0.0.1][=>] GET /hello | query: {} | body: null
|
|
50
|
+
[SpyMW] [<request-id>][127.0.0.1][<=] GET /hello | Took: 1.23 (ms)
|
|
51
|
+
```
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
In **production** mode (`NODE_ENV=production`), body is excluded but query is still logged:
|
|
29
54
|
|
|
30
55
|
```
|
|
31
|
-
[
|
|
32
|
-
[
|
|
56
|
+
[SpyMW] [<request-id>][127.0.0.1][=>] GET /hello | query: {}
|
|
57
|
+
[SpyMW] [<request-id>][127.0.0.1][<=] GET /hello | Took: 1.23 (ms)
|
|
33
58
|
```
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
The log format follows this structure:
|
|
61
|
+
|
|
62
|
+
| Direction | Format |
|
|
63
|
+
|-----------|--------|
|
|
64
|
+
| Incoming (`=>`) | `[requestId][clientIp][=>] METHOD path \| query: {...} \| body: {...}` |
|
|
65
|
+
| Outgoing (`<=`) | `[requestId][clientIp][<=] METHOD path \| Took: X.XX (ms)` |
|
|
66
|
+
|
|
67
|
+
The HTTP method is padded to 8 characters for consistent alignment in log output.
|
|
68
|
+
|
|
69
|
+
> [!TIP]
|
|
70
|
+
> The request ID is also available in error middleware contexts (`NotFoundMiddleware`, `AppErrorMiddleware`), making it easy to correlate error logs with the original request.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
The Request Tracker component has no user-configurable options. Its behavior is fully automatic.
|
|
75
|
+
|
|
76
|
+
| Behavior | Description |
|
|
77
|
+
|----------|-------------|
|
|
78
|
+
| **Request ID** | Generated automatically via `hono/request-id` middleware (UUID), stored on context as `'requestId'` |
|
|
79
|
+
| **IP Detection** | Priority: (1) connection info via `getIncomingIp()`, (2) `x-real-ip` header, (3) `x-forwarded-for` header |
|
|
80
|
+
| **Body Logging** | Logs request body in non-production environments only. Query is always logged |
|
|
81
|
+
| **Timing** | Measures and logs request duration in milliseconds (2 decimal places) via `performance.now()` |
|
|
82
|
+
| **Scope** | Registered as a singleton provider |
|
|
83
|
+
|
|
84
|
+
> [!NOTE]
|
|
85
|
+
> In **production** (`NODE_ENV=production`), only the request **body** is excluded from log output to prevent sensitive data exposure. Query parameters are still logged in all environments.
|
|
86
|
+
|
|
87
|
+
## Internals
|
|
88
|
+
|
|
89
|
+
### RequestTrackerComponent
|
|
90
|
+
|
|
91
|
+
The component class extends `BaseComponent`. It receives `BaseApplication` via `@inject({ key: CoreBindings.APPLICATION_INSTANCE })` in its constructor.
|
|
92
|
+
|
|
93
|
+
During construction, it creates a singleton binding for the middleware:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
Binding.bind({ key: RequestTrackerComponent.REQUEST_TRACKER_MW_BINDING_KEY })
|
|
97
|
+
.toProvider(RequestSpyMiddleware)
|
|
98
|
+
.setScope(BindingScopes.SINGLETON)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The binding key is constructed as `BindingNamespaces.MIDDLEWARE + '.' + RequestSpyMiddleware.name`, which resolves to `'middlewares.RequestSpyMiddleware'`.
|
|
102
|
+
|
|
103
|
+
#### Component Lifecycle
|
|
104
|
+
|
|
105
|
+
1. **`constructor()`** -- Receives `BaseApplication` via DI. Defines the middleware binding as a singleton provider.
|
|
106
|
+
2. **`binding()`** -- Registers `requestId()` middleware on the server. Resolves the `RequestSpyMiddleware` binding from the DI container. Throws if the middleware cannot be resolved. Registers the resolved middleware on the server.
|
|
107
|
+
|
|
108
|
+
### RequestSpyMiddleware
|
|
109
|
+
|
|
110
|
+
The middleware class extends `BaseHelper` with scope `'SpyMW'` and implements `IProvider<MiddlewareHandler>`.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
class RequestSpyMiddleware extends BaseHelper implements IProvider<MiddlewareHandler> {
|
|
114
|
+
static readonly REQUEST_ID_KEY = 'requestId';
|
|
115
|
+
private isDebugMode: boolean;
|
|
116
|
+
// ...
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### IProvider Pattern
|
|
121
|
+
|
|
122
|
+
`RequestSpyMiddleware` implements the `IProvider<T>` interface from `@venizia/ignis-inversion`. This interface requires a single method:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface IProvider<T> {
|
|
126
|
+
value(container: Container): T;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When the DI container resolves the binding (via `.toProvider(RequestSpyMiddleware)`), it instantiates the class and calls `value()` to obtain the actual `MiddlewareHandler`. This pattern allows the middleware to hold state (like `isDebugMode`) while producing a clean middleware function.
|
|
131
|
+
|
|
132
|
+
#### Debug Mode Detection
|
|
133
|
+
|
|
134
|
+
The constructor checks `process.env.NODE_ENV`:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
constructor() {
|
|
138
|
+
super({ scope: 'SpyMW' });
|
|
139
|
+
const env = process.env.NODE_ENV?.toLowerCase();
|
|
140
|
+
this.isDebugMode = env !== Environment.PRODUCTION;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When `isDebugMode` is `true` (any environment other than `'production'`), the incoming request log includes both `query` and `body`. When `false`, only `query` is logged.
|
|
145
|
+
|
|
146
|
+
#### value() -- Middleware Handler
|
|
147
|
+
|
|
148
|
+
The `value()` method returns a Hono middleware created via `createMiddleware()` from `hono/factory`. The middleware performs the following steps:
|
|
149
|
+
|
|
150
|
+
1. Starts a performance timer via `performance.now()`
|
|
151
|
+
2. Extracts the request ID from the Hono context (set by `requestId()` middleware)
|
|
152
|
+
3. Resolves the client IP using the priority chain (see IP Detection below)
|
|
153
|
+
4. Throws `'Malformed Connection Info'` (400) if both `incomingIp` and `forwardedIp` are `null`
|
|
154
|
+
5. Extracts method, path, and query from the request
|
|
155
|
+
6. Parses the request body via `parseBody()`
|
|
156
|
+
7. Logs the incoming request with `[=>]` direction marker
|
|
157
|
+
8. Calls `await next()` to proceed to the next middleware/handler
|
|
158
|
+
9. Calculates duration and logs the outgoing response with `[<=]` direction marker
|
|
159
|
+
|
|
160
|
+
#### parseBody()
|
|
161
|
+
|
|
162
|
+
A public method that parses the request body based on `Content-Type` and `Content-Length` headers.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
async parseBody(opts: { req: TContext['req'] }): Promise<unknown>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Return conditions:**
|
|
169
|
+
|
|
170
|
+
| Condition | Result |
|
|
171
|
+
|-----------|--------|
|
|
172
|
+
| No `Content-Type` header | Returns `null` |
|
|
173
|
+
| No `Content-Length` header or value is `'0'` | Returns `null` |
|
|
174
|
+
| `Content-Type` includes `application/json` | Calls `req.json()` |
|
|
175
|
+
| `Content-Type` includes `multipart/form-data` | Calls `req.parseBody()` |
|
|
176
|
+
| `Content-Type` includes `application/x-www-form-urlencoded` | Calls `req.parseBody()` |
|
|
177
|
+
| Any other `Content-Type` (text, html, xml, etc.) | Calls `req.text()` |
|
|
178
|
+
| Parsing fails for any content type | Throws `'Malformed Body Payload'` (HTTP 400) |
|
|
179
|
+
|
|
180
|
+
### getIncomingIp() Utility
|
|
181
|
+
|
|
182
|
+
A utility function that attempts to extract the client IP address from the Hono context using runtime-specific connection info.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const getIncomingIp = (context: Context): string | null
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Runtime detection:**
|
|
189
|
+
- Uses `RuntimeModules.isBun()` from `@venizia/ignis-helpers` to detect the runtime
|
|
190
|
+
- On **Bun**: imports `getConnInfo` from `hono/bun`
|
|
191
|
+
- On **Node.js**: imports `getConnInfo` from `@hono/node-server/conninfo`
|
|
192
|
+
- Returns `connInfo.remote.address` if available, `null` otherwise
|
|
193
|
+
- Returns `null` if `getConnInfo` is unavailable or throws
|
|
194
|
+
|
|
195
|
+
#### IP Detection Priority
|
|
196
|
+
|
|
197
|
+
The middleware resolves the client IP using a three-step fallback chain:
|
|
198
|
+
|
|
199
|
+
| Priority | Source | Description |
|
|
200
|
+
|----------|--------|-------------|
|
|
201
|
+
| 1 | `getIncomingIp(context)` | Direct connection info from the runtime (Bun or Node.js) |
|
|
202
|
+
| 2 | `x-real-ip` header | Set by reverse proxies (e.g., Nginx `proxy_set_header X-Real-IP`) |
|
|
203
|
+
| 3 | `x-forwarded-for` header | Standard proxy header with original client IP |
|
|
204
|
+
|
|
205
|
+
The `clientIp` used in log output is resolved as `incomingIp ?? forwardedIp` -- meaning connection info takes precedence when available.
|
|
206
|
+
|
|
207
|
+
If **all three** sources return `null`, the middleware throws a `'Malformed Connection Info'` error with HTTP 400 status.
|
|
208
|
+
|
|
209
|
+
## Binding Keys
|
|
210
|
+
|
|
211
|
+
| Key | Constant | Type | Required | Default |
|
|
212
|
+
|-----|----------|------|----------|---------|
|
|
213
|
+
| `middlewares.RequestSpyMiddleware` | `RequestTrackerComponent.REQUEST_TRACKER_MW_BINDING_KEY` | `MiddlewareHandler` | Auto | Provided by component |
|
|
214
|
+
|
|
215
|
+
The key is constructed from `BindingNamespaces.MIDDLEWARE` (`'middlewares'`) + `RequestSpyMiddleware.name` (`'RequestSpyMiddleware'`). The component binds `RequestSpyMiddleware` as a singleton provider at this key during construction.
|
|
216
|
+
|
|
217
|
+
## Troubleshooting
|
|
218
|
+
|
|
219
|
+
### "Invalid middleware to init request tracker | Please check again binding value"
|
|
220
|
+
|
|
221
|
+
**Cause:** The `RequestSpyMiddleware` binding could not be resolved from the DI container during the component's `binding()` phase. This typically means the binding was removed or overwritten before `binding()` executed.
|
|
222
|
+
|
|
223
|
+
**Fix:** Ensure no custom code unbinds or replaces the `middlewares.RequestSpyMiddleware` key. If you need to customize request logging, extend the component rather than removing the binding.
|
|
224
|
+
|
|
225
|
+
### "Malformed Body Payload"
|
|
226
|
+
|
|
227
|
+
**Cause:** The `RequestSpyMiddleware` attempted to parse the request body based on the `Content-Type` header, but the body content was malformed (e.g., invalid JSON with `application/json` content type, or corrupt form data).
|
|
228
|
+
|
|
229
|
+
**Fix:** Ensure clients send valid body content that matches the declared `Content-Type` header. This error returns HTTP 400 Bad Request.
|
|
230
|
+
|
|
231
|
+
### "Malformed Connection Info"
|
|
232
|
+
|
|
233
|
+
**Cause:** The middleware could not determine the client IP address from any source. All three must have failed: (1) `getIncomingIp()` returned `null` (runtime connection info unavailable), (2) `x-real-ip` header was absent, and (3) `x-forwarded-for` header was absent. This error returns HTTP 400 Bad Request.
|
|
234
|
+
|
|
235
|
+
**Fix:** Ensure your reverse proxy (e.g., Nginx, Caddy) forwards at least one of these headers:
|
|
236
|
+
- `x-real-ip`
|
|
237
|
+
- `x-forwarded-for`
|
|
238
|
+
|
|
239
|
+
If running without a proxy, ensure the runtime provides connection info (Bun does this natively; Node.js requires `@hono/node-server`).
|
|
36
240
|
|
|
37
241
|
## See Also
|
|
38
242
|
|
|
39
|
-
- **
|
|
243
|
+
- **Guides:**
|
|
40
244
|
- [Components Overview](/guides/core-concepts/components) - Component system basics
|
|
41
245
|
- [Middlewares](/references/base/middlewares) - Request middleware system
|
|
42
246
|
|
|
43
|
-
- **
|
|
44
|
-
- [Components
|
|
247
|
+
- **Components:**
|
|
248
|
+
- [All Components](./index) - Built-in components list
|
|
45
249
|
|
|
46
|
-
- **
|
|
47
|
-
- [Logger Helper](/references/helpers/logger) - Logging utilities
|
|
250
|
+
- **Helpers:**
|
|
251
|
+
- [Logger Helper](/references/helpers/logger/) - Logging utilities
|
|
48
252
|
|
|
49
253
|
- **Best Practices:**
|
|
50
254
|
- [Troubleshooting Tips](/best-practices/troubleshooting-tips) - Debugging with request IDs
|