@stacksjs/ts-cloud-core 0.1.1
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/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Templates with Stacks Views
|
|
3
|
+
*
|
|
4
|
+
* Provides template management and rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface EmailTemplate {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
subject: string
|
|
11
|
+
html: string
|
|
12
|
+
text?: string
|
|
13
|
+
variables: string[]
|
|
14
|
+
category?: string
|
|
15
|
+
createdAt: string
|
|
16
|
+
updatedAt: string
|
|
17
|
+
version: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TemplateRenderOptions {
|
|
21
|
+
data: Record<string, any>
|
|
22
|
+
locale?: string
|
|
23
|
+
timezone?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Email Templates Module
|
|
28
|
+
*/
|
|
29
|
+
export class EmailTemplates {
|
|
30
|
+
/**
|
|
31
|
+
* Lambda code for template rendering
|
|
32
|
+
*/
|
|
33
|
+
static TemplateRendererCode = `
|
|
34
|
+
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
35
|
+
|
|
36
|
+
const s3 = new S3Client({});
|
|
37
|
+
const TEMPLATE_BUCKET = process.env.TEMPLATE_BUCKET;
|
|
38
|
+
|
|
39
|
+
exports.handler = async (event) => {
|
|
40
|
+
console.log('Template render request:', JSON.stringify(event, null, 2));
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const { templateId, data, locale, timezone } = JSON.parse(event.body || '{}');
|
|
44
|
+
|
|
45
|
+
if (!templateId) {
|
|
46
|
+
return {
|
|
47
|
+
statusCode: 400,
|
|
48
|
+
body: JSON.stringify({ error: 'Missing templateId' }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get template
|
|
53
|
+
const templateKey = \`templates/\${templateId}.json\`;
|
|
54
|
+
const result = await s3.send(new GetObjectCommand({
|
|
55
|
+
Bucket: TEMPLATE_BUCKET,
|
|
56
|
+
Key: templateKey,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const template = JSON.parse(await result.Body.transformToString());
|
|
60
|
+
|
|
61
|
+
// Render template
|
|
62
|
+
const rendered = {
|
|
63
|
+
subject: renderTemplate(template.subject, data),
|
|
64
|
+
html: renderTemplate(template.html, data),
|
|
65
|
+
text: template.text ? renderTemplate(template.text, data) : null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
statusCode: 200,
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify(rendered),
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error rendering template:', error);
|
|
75
|
+
return {
|
|
76
|
+
statusCode: error.name === 'NoSuchKey' ? 404 : 500,
|
|
77
|
+
body: JSON.stringify({ error: error.message }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function renderTemplate(template, data) {
|
|
83
|
+
if (!template || !data) return template;
|
|
84
|
+
|
|
85
|
+
let result = template;
|
|
86
|
+
|
|
87
|
+
// Handle {{variable}} syntax
|
|
88
|
+
result = result.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (match, key) => {
|
|
89
|
+
const value = getNestedValue(data, key);
|
|
90
|
+
return value !== undefined ? String(value) : match;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle {{#if condition}}...{{/if}} syntax
|
|
94
|
+
result = result.replace(/\\{\\{#if\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g, (match, key, content) => {
|
|
95
|
+
const value = getNestedValue(data, key);
|
|
96
|
+
return value ? content : '';
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Handle {{#unless condition}}...{{/unless}} syntax
|
|
100
|
+
result = result.replace(/\\{\\{#unless\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/unless\\}\\}/g, (match, key, content) => {
|
|
101
|
+
const value = getNestedValue(data, key);
|
|
102
|
+
return !value ? content : '';
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Handle {{#each array}}...{{/each}} syntax
|
|
106
|
+
result = result.replace(/\\{\\{#each\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g, (match, key, content) => {
|
|
107
|
+
const array = getNestedValue(data, key);
|
|
108
|
+
if (!Array.isArray(array)) return '';
|
|
109
|
+
return array.map((item, index) => {
|
|
110
|
+
let itemContent = content;
|
|
111
|
+
itemContent = itemContent.replace(/\\{\\{this\\}\\}/g, String(item));
|
|
112
|
+
itemContent = itemContent.replace(/\\{\\{@index\\}\\}/g, String(index));
|
|
113
|
+
if (typeof item === 'object') {
|
|
114
|
+
Object.entries(item).forEach(([k, v]) => {
|
|
115
|
+
itemContent = itemContent.replace(new RegExp(\`\\\\{\\\\{\\\\s*\${k}\\\\s*\\\\}\\\\}\`, 'g'), String(v));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return itemContent;
|
|
119
|
+
}).join('');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getNestedValue(obj, path) {
|
|
126
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
127
|
+
}
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Built-in email templates
|
|
132
|
+
*/
|
|
133
|
+
static BuiltInTemplates = {
|
|
134
|
+
welcome: {
|
|
135
|
+
id: 'welcome',
|
|
136
|
+
name: 'Welcome Email',
|
|
137
|
+
subject: 'Welcome to {{appName}}!',
|
|
138
|
+
html: `
|
|
139
|
+
<!DOCTYPE html>
|
|
140
|
+
<html>
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="utf-8">
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
144
|
+
<title>Welcome</title>
|
|
145
|
+
</head>
|
|
146
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
147
|
+
<div style="text-align: center; margin-bottom: 30px;">
|
|
148
|
+
<h1 style="color: #2563eb;">Welcome to {{appName}}!</h1>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<p>Hi {{userName}},</p>
|
|
152
|
+
|
|
153
|
+
<p>Thank you for joining {{appName}}! We're excited to have you on board.</p>
|
|
154
|
+
|
|
155
|
+
{{#if verificationUrl}}
|
|
156
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
157
|
+
<a href="{{verificationUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Verify Your Email</a>
|
|
158
|
+
</div>
|
|
159
|
+
{{/if}}
|
|
160
|
+
|
|
161
|
+
<p>If you have any questions, feel free to reply to this email.</p>
|
|
162
|
+
|
|
163
|
+
<p>Best regards,<br>The {{appName}} Team</p>
|
|
164
|
+
|
|
165
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
|
166
|
+
<p style="font-size: 12px; color: #666;">
|
|
167
|
+
You received this email because you signed up for {{appName}}.
|
|
168
|
+
</p>
|
|
169
|
+
</body>
|
|
170
|
+
</html>`,
|
|
171
|
+
text: `Welcome to {{appName}}!
|
|
172
|
+
|
|
173
|
+
Hi {{userName}},
|
|
174
|
+
|
|
175
|
+
Thank you for joining {{appName}}! We're excited to have you on board.
|
|
176
|
+
|
|
177
|
+
{{#if verificationUrl}}
|
|
178
|
+
Verify your email: {{verificationUrl}}
|
|
179
|
+
{{/if}}
|
|
180
|
+
|
|
181
|
+
If you have any questions, feel free to reply to this email.
|
|
182
|
+
|
|
183
|
+
Best regards,
|
|
184
|
+
The {{appName}} Team`,
|
|
185
|
+
variables: ['appName', 'userName', 'verificationUrl'] as const,
|
|
186
|
+
category: 'onboarding',
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
passwordReset: {
|
|
190
|
+
id: 'password-reset',
|
|
191
|
+
name: 'Password Reset',
|
|
192
|
+
subject: 'Reset your {{appName}} password',
|
|
193
|
+
html: `
|
|
194
|
+
<!DOCTYPE html>
|
|
195
|
+
<html>
|
|
196
|
+
<head>
|
|
197
|
+
<meta charset="utf-8">
|
|
198
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
199
|
+
</head>
|
|
200
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
201
|
+
<h1 style="color: #2563eb;">Password Reset Request</h1>
|
|
202
|
+
|
|
203
|
+
<p>Hi {{userName}},</p>
|
|
204
|
+
|
|
205
|
+
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
|
206
|
+
|
|
207
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
208
|
+
<a href="{{resetUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Reset Password</a>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<p>This link will expire in {{expiresIn}}.</p>
|
|
212
|
+
|
|
213
|
+
<p>If you didn't request this, you can safely ignore this email.</p>
|
|
214
|
+
|
|
215
|
+
<p>Best regards,<br>The {{appName}} Team</p>
|
|
216
|
+
</body>
|
|
217
|
+
</html>`,
|
|
218
|
+
text: `Password Reset Request
|
|
219
|
+
|
|
220
|
+
Hi {{userName}},
|
|
221
|
+
|
|
222
|
+
We received a request to reset your password. Visit the link below to create a new password:
|
|
223
|
+
|
|
224
|
+
{{resetUrl}}
|
|
225
|
+
|
|
226
|
+
This link will expire in {{expiresIn}}.
|
|
227
|
+
|
|
228
|
+
If you didn't request this, you can safely ignore this email.
|
|
229
|
+
|
|
230
|
+
Best regards,
|
|
231
|
+
The {{appName}} Team`,
|
|
232
|
+
variables: ['appName', 'userName', 'resetUrl', 'expiresIn'] as const,
|
|
233
|
+
category: 'auth',
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
notification: {
|
|
237
|
+
id: 'notification',
|
|
238
|
+
name: 'Notification',
|
|
239
|
+
subject: '{{subject}}',
|
|
240
|
+
html: `
|
|
241
|
+
<!DOCTYPE html>
|
|
242
|
+
<html>
|
|
243
|
+
<head>
|
|
244
|
+
<meta charset="utf-8">
|
|
245
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
246
|
+
</head>
|
|
247
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
248
|
+
<h1 style="color: #2563eb;">{{title}}</h1>
|
|
249
|
+
|
|
250
|
+
<p>{{message}}</p>
|
|
251
|
+
|
|
252
|
+
{{#if actionUrl}}
|
|
253
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
254
|
+
<a href="{{actionUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">{{actionText}}</a>
|
|
255
|
+
</div>
|
|
256
|
+
{{/if}}
|
|
257
|
+
|
|
258
|
+
<p style="font-size: 12px; color: #666; margin-top: 30px;">
|
|
259
|
+
— {{appName}}
|
|
260
|
+
</p>
|
|
261
|
+
</body>
|
|
262
|
+
</html>`,
|
|
263
|
+
text: `{{title}}
|
|
264
|
+
|
|
265
|
+
{{message}}
|
|
266
|
+
|
|
267
|
+
{{#if actionUrl}}
|
|
268
|
+
{{actionText}}: {{actionUrl}}
|
|
269
|
+
{{/if}}
|
|
270
|
+
|
|
271
|
+
— {{appName}}`,
|
|
272
|
+
variables: ['subject', 'title', 'message', 'actionUrl', 'actionText', 'appName'] as const,
|
|
273
|
+
category: 'notification',
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
invoice: {
|
|
277
|
+
id: 'invoice',
|
|
278
|
+
name: 'Invoice',
|
|
279
|
+
subject: 'Invoice #{{invoiceNumber}} from {{appName}}',
|
|
280
|
+
html: `
|
|
281
|
+
<!DOCTYPE html>
|
|
282
|
+
<html>
|
|
283
|
+
<head>
|
|
284
|
+
<meta charset="utf-8">
|
|
285
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
286
|
+
</head>
|
|
287
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
288
|
+
<h1 style="color: #2563eb;">Invoice #{{invoiceNumber}}</h1>
|
|
289
|
+
|
|
290
|
+
<p>Hi {{customerName}},</p>
|
|
291
|
+
|
|
292
|
+
<p>Here's your invoice for {{period}}.</p>
|
|
293
|
+
|
|
294
|
+
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
|
|
295
|
+
<thead>
|
|
296
|
+
<tr style="background-color: #f3f4f6;">
|
|
297
|
+
<th style="padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb;">Item</th>
|
|
298
|
+
<th style="padding: 10px; text-align: right; border-bottom: 1px solid #e5e7eb;">Amount</th>
|
|
299
|
+
</tr>
|
|
300
|
+
</thead>
|
|
301
|
+
<tbody>
|
|
302
|
+
{{#each items}}
|
|
303
|
+
<tr>
|
|
304
|
+
<td style="padding: 10px; border-bottom: 1px solid #e5e7eb;">{{description}}</td>
|
|
305
|
+
<td style="padding: 10px; text-align: right; border-bottom: 1px solid #e5e7eb;">{{amount}}</td>
|
|
306
|
+
</tr>
|
|
307
|
+
{{/each}}
|
|
308
|
+
</tbody>
|
|
309
|
+
<tfoot>
|
|
310
|
+
<tr>
|
|
311
|
+
<td style="padding: 10px; font-weight: bold;">Total</td>
|
|
312
|
+
<td style="padding: 10px; text-align: right; font-weight: bold;">{{total}}</td>
|
|
313
|
+
</tr>
|
|
314
|
+
</tfoot>
|
|
315
|
+
</table>
|
|
316
|
+
|
|
317
|
+
{{#if paymentUrl}}
|
|
318
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
319
|
+
<a href="{{paymentUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Pay Now</a>
|
|
320
|
+
</div>
|
|
321
|
+
{{/if}}
|
|
322
|
+
|
|
323
|
+
<p>Thank you for your business!</p>
|
|
324
|
+
|
|
325
|
+
<p>Best regards,<br>The {{appName}} Team</p>
|
|
326
|
+
</body>
|
|
327
|
+
</html>`,
|
|
328
|
+
variables: ['invoiceNumber', 'customerName', 'period', 'items', 'total', 'paymentUrl', 'appName'] as const,
|
|
329
|
+
category: 'billing',
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create template storage bucket
|
|
335
|
+
*/
|
|
336
|
+
static createTemplateBucket(config: { slug: string }): Record<string, any> {
|
|
337
|
+
return {
|
|
338
|
+
[`${config.slug}EmailTemplateBucket`]: {
|
|
339
|
+
Type: 'AWS::S3::Bucket',
|
|
340
|
+
Properties: {
|
|
341
|
+
BucketName: `${config.slug}-email-templates`,
|
|
342
|
+
VersioningConfiguration: {
|
|
343
|
+
Status: 'Enabled',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create template renderer Lambda
|
|
352
|
+
*/
|
|
353
|
+
static createTemplateRendererLambda(config: {
|
|
354
|
+
slug: string
|
|
355
|
+
roleArn: string
|
|
356
|
+
templateBucket: string
|
|
357
|
+
}): Record<string, any> {
|
|
358
|
+
return {
|
|
359
|
+
[`${config.slug}TemplateRendererLambda`]: {
|
|
360
|
+
Type: 'AWS::Lambda::Function',
|
|
361
|
+
Properties: {
|
|
362
|
+
FunctionName: `${config.slug}-template-renderer`,
|
|
363
|
+
Runtime: 'nodejs20.x',
|
|
364
|
+
Handler: 'index.handler',
|
|
365
|
+
Role: config.roleArn,
|
|
366
|
+
Timeout: 30,
|
|
367
|
+
MemorySize: 256,
|
|
368
|
+
Code: {
|
|
369
|
+
ZipFile: EmailTemplates.TemplateRendererCode,
|
|
370
|
+
},
|
|
371
|
+
Environment: {
|
|
372
|
+
Variables: {
|
|
373
|
+
TEMPLATE_BUCKET: config.templateBucket,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Render a template with data (SDK helper)
|
|
383
|
+
*/
|
|
384
|
+
static render(template: string, data: Record<string, any>): string {
|
|
385
|
+
if (!template || !data) return template
|
|
386
|
+
|
|
387
|
+
let result = template
|
|
388
|
+
|
|
389
|
+
// Handle {{variable}} syntax
|
|
390
|
+
result = result.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => {
|
|
391
|
+
const value = EmailTemplates.getNestedValue(data, key)
|
|
392
|
+
return value !== undefined ? String(value) : match
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Handle {{#if condition}}...{{/if}} syntax
|
|
396
|
+
result = result.replace(/\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, key, content) => {
|
|
397
|
+
const value = EmailTemplates.getNestedValue(data, key)
|
|
398
|
+
return value ? content : ''
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// Handle {{#unless condition}}...{{/unless}} syntax
|
|
402
|
+
result = result.replace(/\{\{#unless\s+([\w.]+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (match, key, content) => {
|
|
403
|
+
const value = EmailTemplates.getNestedValue(data, key)
|
|
404
|
+
return !value ? content : ''
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
// Handle {{#each array}}...{{/each}} syntax
|
|
408
|
+
result = result.replace(/\{\{#each\s+([\w.]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => {
|
|
409
|
+
const array = EmailTemplates.getNestedValue(data, key)
|
|
410
|
+
if (!Array.isArray(array)) return ''
|
|
411
|
+
return array.map((item, index) => {
|
|
412
|
+
let itemContent = content
|
|
413
|
+
itemContent = itemContent.replace(/\{\{this\}\}/g, String(item))
|
|
414
|
+
itemContent = itemContent.replace(/\{\{@index\}\}/g, String(index))
|
|
415
|
+
if (typeof item === 'object') {
|
|
416
|
+
Object.entries(item).forEach(([k, v]) => {
|
|
417
|
+
itemContent = itemContent.replace(new RegExp(`\\{\\{\\s*${k}\\s*\\}\\}`, 'g'), String(v))
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
return itemContent
|
|
421
|
+
}).join('')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
return result
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private static getNestedValue(obj: Record<string, any>, path: string): any {
|
|
428
|
+
return path.split('.').reduce((current, key) => current?.[key], obj)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Extract variables from template
|
|
433
|
+
*/
|
|
434
|
+
static extractVariables(template: string): string[] {
|
|
435
|
+
const variables = new Set<string>()
|
|
436
|
+
|
|
437
|
+
// Match {{variable}}
|
|
438
|
+
const matches = template.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)
|
|
439
|
+
for (const match of matches) {
|
|
440
|
+
if (!match[1].startsWith('#') && !match[1].startsWith('/') && !match[1].startsWith('@')) {
|
|
441
|
+
variables.add(match[1].split('.')[0])
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Match {{#if variable}}
|
|
446
|
+
const ifMatches = template.matchAll(/\{\{#(?:if|unless|each)\s+([\w.]+)\}\}/g)
|
|
447
|
+
for (const match of ifMatches) {
|
|
448
|
+
variables.add(match[1].split('.')[0])
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return Array.from(variables)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export default EmailTemplates
|