feat: Add AWS SES v2 email service
Complete transactional email layer for Elmeg: - 3 SES templates (verification, password reset, security alert) - TypeScript integration module with AWS SDK v3 - Template deployment script - Usage examples - Comprehensive README with compliance notes
This commit is contained in:
parent
da5b5e7c45
commit
530f217445
7 changed files with 1131 additions and 0 deletions
147
email/README.md
Normal file
147
email/README.md
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
# Elmeg Email Service
|
||||||
|
|
||||||
|
Transactional email layer for Elmeg using Amazon SES v2 with stored templates.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This module provides a production-ready email service for user-initiated transactional emails:
|
||||||
|
|
||||||
|
- **Email Verification** – Sent after user registration
|
||||||
|
- **Password Reset** – Sent when user requests password recovery
|
||||||
|
- **Security Alerts** – Sent for account security events (new logins, password changes)
|
||||||
|
|
||||||
|
> **Compliance Note:** This service is strictly for transactional, user-initiated emails. No newsletters, marketing emails, or cold outreach. No purchased or third-party email lists are used.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js >= 18.0.0
|
||||||
|
- AWS account with SES verified domain
|
||||||
|
- SES templates deployed (see below)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
AWS_ACCESS_KEY_ID=AKIA... # IAM user with SES permissions
|
||||||
|
AWS_SECRET_ACCESS_KEY=... # IAM user secret key
|
||||||
|
AWS_SES_REGION=us-east-1 # SES region (domain must be verified here)
|
||||||
|
EMAIL_FROM=noreply@elmeg.xyz # Verified sender address
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
FRONTEND_URL=https://elmeg.xyz # For generating email links
|
||||||
|
SUPPORT_EMAIL=support@elmeg.xyz # Contact email in templates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd email
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy Templates to SES
|
||||||
|
|
||||||
|
Before sending emails, deploy the templates to AWS SES:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy-templates
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates/updates three templates in SES:
|
||||||
|
|
||||||
|
- `ELMEG_EMAIL_VERIFICATION`
|
||||||
|
- `ELMEG_PASSWORD_RESET`
|
||||||
|
- `ELMEG_SECURITY_ALERT`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
sendVerificationEmail,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
sendSecurityAlertEmail,
|
||||||
|
generateVerificationLink,
|
||||||
|
generateResetLink,
|
||||||
|
} from "@elmeg/email-service";
|
||||||
|
|
||||||
|
// After user registration
|
||||||
|
await sendVerificationEmail({
|
||||||
|
to: "user@example.com",
|
||||||
|
userName: "John",
|
||||||
|
verificationLink: generateVerificationLink(token),
|
||||||
|
});
|
||||||
|
|
||||||
|
// After password reset request
|
||||||
|
await sendPasswordResetEmail({
|
||||||
|
to: "user@example.com",
|
||||||
|
userName: "John",
|
||||||
|
resetLink: generateResetLink(token),
|
||||||
|
});
|
||||||
|
|
||||||
|
// After suspicious login
|
||||||
|
await sendSecurityAlertEmail({
|
||||||
|
to: "user@example.com",
|
||||||
|
userName: "John",
|
||||||
|
securityEventDescription: "New sign-in from Chrome on Windows at 10:30 AM",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Placeholders
|
||||||
|
|
||||||
|
| Placeholder | Description | Templates |
|
||||||
|
|-------------|-------------|-----------|
|
||||||
|
| `{{app_name}}` | "Elmeg" | All |
|
||||||
|
| `{{user_name}}` | User's name or email prefix | All |
|
||||||
|
| `{{support_email}}` | Support contact | All |
|
||||||
|
| `{{verification_link}}` | Email verification URL | Verification |
|
||||||
|
| `{{reset_link}}` | Password reset URL | Password Reset |
|
||||||
|
| `{{security_event_description}}` | Event details | Security Alert |
|
||||||
|
|
||||||
|
## AWS SES Setup Checklist
|
||||||
|
|
||||||
|
1. **Verify Domain** – Add `elmeg.xyz` in SES console with DKIM records
|
||||||
|
2. **Request Production Access** – Move out of sandbox to send to any address
|
||||||
|
3. **Create IAM User** – With `ses:SendEmail` and `ses:SendTemplatedEmail` permissions
|
||||||
|
4. **Deploy Templates** – Run `npm run deploy-templates`
|
||||||
|
|
||||||
|
## Compliance & Best Practices
|
||||||
|
|
||||||
|
| Requirement | Implementation |
|
||||||
|
|-------------|----------------|
|
||||||
|
| User-initiated only | All emails triggered by user actions |
|
||||||
|
| No purchased lists | Only registered users receive emails |
|
||||||
|
| Bounce handling | SES automatically suppresses bounced addresses |
|
||||||
|
| Complaint handling | SES suppresses addresses that report spam |
|
||||||
|
| Unsubscribe | N/A for transactional (required action emails) |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All send functions return a structured result:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EmailResult {
|
||||||
|
success: boolean;
|
||||||
|
messageId?: string; // SES message ID on success
|
||||||
|
error?: {
|
||||||
|
code: string; // Error code from SES
|
||||||
|
message: string; // Human-readable error message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
email/
|
||||||
|
├── src/
|
||||||
|
│ ├── email-service.ts # Main service module
|
||||||
|
│ └── examples.ts # Usage examples
|
||||||
|
├── scripts/
|
||||||
|
│ └── deploy-templates.ts # Template deployment script
|
||||||
|
├── templates/
|
||||||
|
│ └── README.md # Template documentation
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
33
email/package.json
Normal file
33
email/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "@elmeg/email-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Transactional email service for Elmeg using AWS SES v2",
|
||||||
|
"main": "dist/email-service.js",
|
||||||
|
"types": "dist/email-service.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"deploy-templates": "ts-node scripts/deploy-templates.ts"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"email",
|
||||||
|
"ses",
|
||||||
|
"aws",
|
||||||
|
"transactional"
|
||||||
|
],
|
||||||
|
"author": "Elmeg",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-sesv2": "^3.478.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"ts-node": "^10.9.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"@types/jest": "^29.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
234
email/scripts/deploy-templates.ts
Normal file
234
email/scripts/deploy-templates.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
/**
|
||||||
|
* Deploy SES Templates to AWS
|
||||||
|
*
|
||||||
|
* Run this script to create or update the email templates in AWS SES.
|
||||||
|
* Usage: npx ts-node scripts/deploy-templates.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SESv2Client, CreateEmailTemplateCommand, UpdateEmailTemplateCommand } from "@aws-sdk/client-sesv2";
|
||||||
|
|
||||||
|
const sesClient = new SESv2Client({
|
||||||
|
region: process.env.AWS_SES_REGION || "us-east-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const APP_NAME = "Elmeg";
|
||||||
|
const SUPPORT_EMAIL = "support@elmeg.xyz";
|
||||||
|
|
||||||
|
// Template definitions
|
||||||
|
const templates = [
|
||||||
|
{
|
||||||
|
TemplateName: "ELMEG_EMAIL_VERIFICATION",
|
||||||
|
TemplateContent: {
|
||||||
|
Subject: "Verify your Elmeg account",
|
||||||
|
Html: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Verify your email address</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Thanks for signing up for {{app_name}}. Please verify your email address by clicking the button below.</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #4f46e5; border-radius: 6px;">
|
||||||
|
<a href="{{verification_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Verify Email Address</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">This link will expire after 24 hours for your security.</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">If you did not create an account, you can safely ignore this email.</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">If the button above doesn't work, copy and paste this URL: {{verification_link}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} • Contact: {{support_email}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
Text: `Verify your Elmeg account
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the link below:
|
||||||
|
|
||||||
|
{{verification_link}}
|
||||||
|
|
||||||
|
This link will expire after 24 hours for your security.
|
||||||
|
|
||||||
|
If you did not create an account, you can safely ignore this email.
|
||||||
|
|
||||||
|
---
|
||||||
|
{{app_name}} • Contact: {{support_email}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TemplateName: "ELMEG_PASSWORD_RESET",
|
||||||
|
TemplateContent: {
|
||||||
|
Subject: "Reset your Elmeg password",
|
||||||
|
Html: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Reset your password</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">We received a request to reset the password for your {{app_name}} account. Click the button below to choose a new password.</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #4f46e5; border-radius: 6px;">
|
||||||
|
<a href="{{reset_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Reset Password</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">This link will expire after 1 hour for your security.</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">If you did not request a password reset, you can safely ignore this email.</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">If the button above doesn't work, copy and paste this URL: {{reset_link}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} • Contact: {{support_email}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
Text: `Reset your Elmeg password
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
We received a request to reset the password for your {{app_name}} account.
|
||||||
|
|
||||||
|
Click the link below to choose a new password:
|
||||||
|
|
||||||
|
{{reset_link}}
|
||||||
|
|
||||||
|
This link will expire after 1 hour for your security.
|
||||||
|
|
||||||
|
If you did not request a password reset, you can safely ignore this email.
|
||||||
|
|
||||||
|
---
|
||||||
|
{{app_name}} • Contact: {{support_email}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TemplateName: "ELMEG_SECURITY_ALERT",
|
||||||
|
TemplateContent: {
|
||||||
|
Subject: "Security alert for your Elmeg account",
|
||||||
|
Html: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin-bottom: 20px;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fef3c7; border-radius: 6px; padding: 12px 16px;">
|
||||||
|
<p style="margin: 0; color: #92400e; font-size: 14px; font-weight: 600;">⚠️ Security Notice</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Account activity detected</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">We detected the following activity on your {{app_name}} account:</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 20px 0; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f3f4f6; border-radius: 6px; padding: 16px;">
|
||||||
|
<p style="margin: 0; color: #374151; font-size: 15px; line-height: 1.5;">{{security_event_description}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">If this was you, no further action is needed.</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">If you did not perform this action, please secure your account immediately.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} • Contact: {{support_email}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
Text: `Security alert for your Elmeg account
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
We detected the following activity on your {{app_name}} account:
|
||||||
|
|
||||||
|
{{security_event_description}}
|
||||||
|
|
||||||
|
If this was you, no further action is needed.
|
||||||
|
|
||||||
|
If you did not perform this action, please secure your account immediately.
|
||||||
|
|
||||||
|
---
|
||||||
|
{{app_name}} • Contact: {{support_email}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function deployTemplates() {
|
||||||
|
console.log("🚀 Deploying SES email templates...\n");
|
||||||
|
|
||||||
|
for (const template of templates) {
|
||||||
|
try {
|
||||||
|
// Try to create the template first
|
||||||
|
const createCommand = new CreateEmailTemplateCommand(template);
|
||||||
|
await sesClient.send(createCommand);
|
||||||
|
console.log(`✅ Created template: ${template.TemplateName}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as { name?: string };
|
||||||
|
if (err.name === "AlreadyExistsException") {
|
||||||
|
// Template exists, update it
|
||||||
|
try {
|
||||||
|
const updateCommand = new UpdateEmailTemplateCommand(template);
|
||||||
|
await sesClient.send(updateCommand);
|
||||||
|
console.log(`🔄 Updated template: ${template.TemplateName}`);
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error(`❌ Failed to update ${template.TemplateName}:`, updateError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Failed to create ${template.TemplateName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ Template deployment complete!");
|
||||||
|
}
|
||||||
|
|
||||||
|
deployTemplates().catch(console.error);
|
||||||
209
email/src/email-service.ts
Normal file
209
email/src/email-service.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
/**
|
||||||
|
* Elmeg Email Service - AWS SES v2 Integration
|
||||||
|
*
|
||||||
|
* Transactional email layer for user-initiated emails only.
|
||||||
|
* Uses AWS SES stored templates for consistent, reliable delivery.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
||||||
|
|
||||||
|
// Configuration from environment variables
|
||||||
|
const config = {
|
||||||
|
region: process.env.AWS_SES_REGION || "us-east-1",
|
||||||
|
fromAddress: process.env.EMAIL_FROM || "noreply@elmeg.xyz",
|
||||||
|
appName: "Elmeg",
|
||||||
|
supportEmail: process.env.SUPPORT_EMAIL || "support@elmeg.xyz",
|
||||||
|
frontendUrl: process.env.FRONTEND_URL || "https://elmeg.xyz",
|
||||||
|
};
|
||||||
|
|
||||||
|
// SES Template Names
|
||||||
|
export const SES_TEMPLATES = {
|
||||||
|
EMAIL_VERIFICATION: "ELMEG_EMAIL_VERIFICATION",
|
||||||
|
PASSWORD_RESET: "ELMEG_PASSWORD_RESET",
|
||||||
|
SECURITY_ALERT: "ELMEG_SECURITY_ALERT",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Initialize SES v2 client
|
||||||
|
const sesClient = new SESv2Client({
|
||||||
|
region: config.region,
|
||||||
|
// Credentials are loaded automatically from env vars:
|
||||||
|
// AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface SendVerificationEmailParams {
|
||||||
|
to: string;
|
||||||
|
userName: string;
|
||||||
|
verificationLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendPasswordResetEmailParams {
|
||||||
|
to: string;
|
||||||
|
userName: string;
|
||||||
|
resetLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendSecurityAlertEmailParams {
|
||||||
|
to: string;
|
||||||
|
userName: string;
|
||||||
|
securityEventDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailResult {
|
||||||
|
success: boolean;
|
||||||
|
messageId?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailError extends Error {
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
constructor(code: string, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "EmailError";
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Email Sending Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email verification email to new users
|
||||||
|
*/
|
||||||
|
export async function sendVerificationEmail(
|
||||||
|
params: SendVerificationEmailParams
|
||||||
|
): Promise<EmailResult> {
|
||||||
|
const templateData = {
|
||||||
|
user_name: params.userName,
|
||||||
|
verification_link: params.verificationLink,
|
||||||
|
app_name: config.appName,
|
||||||
|
support_email: config.supportEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sendTemplatedEmail(
|
||||||
|
params.to,
|
||||||
|
SES_TEMPLATES.EMAIL_VERIFICATION,
|
||||||
|
templateData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password reset email
|
||||||
|
*/
|
||||||
|
export async function sendPasswordResetEmail(
|
||||||
|
params: SendPasswordResetEmailParams
|
||||||
|
): Promise<EmailResult> {
|
||||||
|
const templateData = {
|
||||||
|
user_name: params.userName,
|
||||||
|
reset_link: params.resetLink,
|
||||||
|
app_name: config.appName,
|
||||||
|
support_email: config.supportEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sendTemplatedEmail(
|
||||||
|
params.to,
|
||||||
|
SES_TEMPLATES.PASSWORD_RESET,
|
||||||
|
templateData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send security alert email for account events
|
||||||
|
*/
|
||||||
|
export async function sendSecurityAlertEmail(
|
||||||
|
params: SendSecurityAlertEmailParams
|
||||||
|
): Promise<EmailResult> {
|
||||||
|
const templateData = {
|
||||||
|
user_name: params.userName,
|
||||||
|
security_event_description: params.securityEventDescription,
|
||||||
|
app_name: config.appName,
|
||||||
|
support_email: config.supportEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sendTemplatedEmail(
|
||||||
|
params.to,
|
||||||
|
SES_TEMPLATES.SECURITY_ALERT,
|
||||||
|
templateData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Core Email Function
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function sendTemplatedEmail(
|
||||||
|
to: string,
|
||||||
|
templateName: string,
|
||||||
|
templateData: Record<string, string>
|
||||||
|
): Promise<EmailResult> {
|
||||||
|
try {
|
||||||
|
const command = new SendEmailCommand({
|
||||||
|
FromEmailAddress: config.fromAddress,
|
||||||
|
Destination: {
|
||||||
|
ToAddresses: [to],
|
||||||
|
},
|
||||||
|
Content: {
|
||||||
|
Template: {
|
||||||
|
TemplateName: templateName,
|
||||||
|
TemplateData: JSON.stringify(templateData),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await sesClient.send(command);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: response.MessageId,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as { name?: string; message?: string; Code?: string };
|
||||||
|
|
||||||
|
console.error(`[Email] Failed to send ${templateName} to ${to}:`, err.message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: err.Code || err.name || "UNKNOWN_ERROR",
|
||||||
|
message: err.message || "Failed to send email",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Utility Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a verification link for a user
|
||||||
|
*/
|
||||||
|
export function generateVerificationLink(token: string): string {
|
||||||
|
return `${config.frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a password reset link for a user
|
||||||
|
*/
|
||||||
|
export function generateResetLink(token: string): string {
|
||||||
|
return `${config.frontendUrl}/reset-password?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the email service is properly configured
|
||||||
|
*/
|
||||||
|
export function isEmailConfigured(): boolean {
|
||||||
|
return !!(
|
||||||
|
process.env.AWS_ACCESS_KEY_ID &&
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY &&
|
||||||
|
process.env.AWS_SES_REGION
|
||||||
|
);
|
||||||
|
}
|
||||||
176
email/src/examples.ts
Normal file
176
email/src/examples.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* Elmeg Email Service - Usage Examples
|
||||||
|
*
|
||||||
|
* These examples show how to integrate the email service
|
||||||
|
* into your application's user flows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
sendVerificationEmail,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
sendSecurityAlertEmail,
|
||||||
|
generateVerificationLink,
|
||||||
|
generateResetLink,
|
||||||
|
isEmailConfigured,
|
||||||
|
} from "./email-service";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example 1: User Registration Flow
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function handleUserRegistration(
|
||||||
|
userEmail: string,
|
||||||
|
userName: string,
|
||||||
|
verificationToken: string
|
||||||
|
) {
|
||||||
|
// Check if email is configured
|
||||||
|
if (!isEmailConfigured()) {
|
||||||
|
console.warn("[Email] Email service not configured, skipping verification email");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the verification link
|
||||||
|
const verificationLink = generateVerificationLink(verificationToken);
|
||||||
|
|
||||||
|
// Send the verification email
|
||||||
|
const result = await sendVerificationEmail({
|
||||||
|
to: userEmail,
|
||||||
|
userName: userName || userEmail.split("@")[0],
|
||||||
|
verificationLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`[Email] Verification email sent to ${userEmail}, messageId: ${result.messageId}`);
|
||||||
|
} else {
|
||||||
|
console.error(`[Email] Failed to send verification email: ${result.error?.message}`);
|
||||||
|
// Handle error - maybe retry or alert admin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example 2: Forgot Password Flow
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function handleForgotPassword(
|
||||||
|
userEmail: string,
|
||||||
|
userName: string,
|
||||||
|
resetToken: string
|
||||||
|
) {
|
||||||
|
if (!isEmailConfigured()) {
|
||||||
|
console.warn("[Email] Email service not configured, skipping password reset email");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLink = generateResetLink(resetToken);
|
||||||
|
|
||||||
|
const result = await sendPasswordResetEmail({
|
||||||
|
to: userEmail,
|
||||||
|
userName: userName || userEmail.split("@")[0],
|
||||||
|
resetLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`[Email] Password reset email sent to ${userEmail}`);
|
||||||
|
} else {
|
||||||
|
console.error(`[Email] Failed to send password reset email: ${result.error?.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example 3: Security Alert - New Login
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function handleNewLogin(
|
||||||
|
userEmail: string,
|
||||||
|
userName: string,
|
||||||
|
loginDetails: { ip: string; browser: string; location?: string; timestamp: Date }
|
||||||
|
) {
|
||||||
|
if (!isEmailConfigured()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventDescription = [
|
||||||
|
`New sign-in to your account`,
|
||||||
|
``,
|
||||||
|
`Time: ${loginDetails.timestamp.toLocaleString()}`,
|
||||||
|
`IP Address: ${loginDetails.ip}`,
|
||||||
|
`Browser: ${loginDetails.browser}`,
|
||||||
|
loginDetails.location ? `Location: ${loginDetails.location}` : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const result = await sendSecurityAlertEmail({
|
||||||
|
to: userEmail,
|
||||||
|
userName: userName || userEmail.split("@")[0],
|
||||||
|
securityEventDescription: eventDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[Email] Failed to send security alert: ${result.error?.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example 4: Security Alert - Password Changed
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function handlePasswordChanged(
|
||||||
|
userEmail: string,
|
||||||
|
userName: string,
|
||||||
|
timestamp: Date
|
||||||
|
) {
|
||||||
|
if (!isEmailConfigured()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventDescription = `Your password was changed on ${timestamp.toLocaleString()}. If you did not make this change, please contact support immediately.`;
|
||||||
|
|
||||||
|
await sendSecurityAlertEmail({
|
||||||
|
to: userEmail,
|
||||||
|
userName: userName || userEmail.split("@")[0],
|
||||||
|
securityEventDescription: eventDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example 5: Express.js Route Handler Integration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import express from "express";
|
||||||
|
import { sendVerificationEmail, generateVerificationLink } from "./email-service";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/register", async (req, res) => {
|
||||||
|
const { email, password, name } = req.body;
|
||||||
|
|
||||||
|
// ... create user in database ...
|
||||||
|
const user = await createUser({ email, password, name });
|
||||||
|
|
||||||
|
// Generate verification token
|
||||||
|
const verificationToken = generateSecureToken();
|
||||||
|
await saveVerificationToken(user.id, verificationToken);
|
||||||
|
|
||||||
|
// Send verification email
|
||||||
|
const verificationLink = generateVerificationLink(verificationToken);
|
||||||
|
|
||||||
|
const emailResult = await sendVerificationEmail({
|
||||||
|
to: email,
|
||||||
|
userName: name || email.split("@")[0],
|
||||||
|
verificationLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailResult.success) {
|
||||||
|
console.error("Failed to send verification email:", emailResult.error);
|
||||||
|
// Don't fail registration, just log the error
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: "Account created. Please check your email to verify your account."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
*/
|
||||||
306
email/templates/README.md
Normal file
306
email/templates/README.md
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
# AWS SES Email Templates for Elmeg
|
||||||
|
|
||||||
|
## Template 1: Email Verification
|
||||||
|
|
||||||
|
**Template Name:** `ELMEG_EMAIL_VERIFICATION`
|
||||||
|
|
||||||
|
### Subject
|
||||||
|
|
||||||
|
```
|
||||||
|
Verify your Elmeg account
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Body
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Verify your email</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Verify your email address</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
Hi {{user_name}},
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the button below.
|
||||||
|
</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #4f46e5; border-radius: 6px;">
|
||||||
|
<a href="{{verification_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Verify Email Address</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
|
||||||
|
This link will expire after 24 hours for your security.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
|
||||||
|
If you did not create an account, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
||||||
|
If the button above doesn't work, copy and paste this URL into your browser:
|
||||||
|
</p>
|
||||||
|
<p style="margin: 10px 0 0; color: #4f46e5; font-size: 12px; word-break: break-all;">
|
||||||
|
{{verification_link}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
|
Questions? Contact us at {{support_email}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plain Text Body
|
||||||
|
|
||||||
|
```
|
||||||
|
Verify your Elmeg account
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the link below:
|
||||||
|
|
||||||
|
{{verification_link}}
|
||||||
|
|
||||||
|
This link will expire after 24 hours for your security.
|
||||||
|
|
||||||
|
If you did not create an account, you can safely ignore this email.
|
||||||
|
|
||||||
|
---
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
Questions? Contact us at {{support_email}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template 2: Password Reset
|
||||||
|
|
||||||
|
**Template Name:** `ELMEG_PASSWORD_RESET`
|
||||||
|
|
||||||
|
### Subject
|
||||||
|
|
||||||
|
```
|
||||||
|
Reset your Elmeg password
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Body
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reset your password</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Reset your password</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
Hi {{user_name}},
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
We received a request to reset the password for your {{app_name}} account. Click the button below to choose a new password.
|
||||||
|
</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #4f46e5; border-radius: 6px;">
|
||||||
|
<a href="{{reset_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Reset Password</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
|
||||||
|
This link will expire after 1 hour for your security.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
|
||||||
|
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
||||||
|
</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
||||||
|
If the button above doesn't work, copy and paste this URL into your browser:
|
||||||
|
</p>
|
||||||
|
<p style="margin: 10px 0 0; color: #4f46e5; font-size: 12px; word-break: break-all;">
|
||||||
|
{{reset_link}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
|
Questions? Contact us at {{support_email}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plain Text Body
|
||||||
|
|
||||||
|
```
|
||||||
|
Reset your Elmeg password
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
We received a request to reset the password for your {{app_name}} account.
|
||||||
|
|
||||||
|
Click the link below to choose a new password:
|
||||||
|
|
||||||
|
{{reset_link}}
|
||||||
|
|
||||||
|
This link will expire after 1 hour for your security.
|
||||||
|
|
||||||
|
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
Questions? Contact us at {{support_email}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template 3: Security Alert
|
||||||
|
|
||||||
|
**Template Name:** `ELMEG_SECURITY_ALERT`
|
||||||
|
|
||||||
|
### Subject
|
||||||
|
|
||||||
|
```
|
||||||
|
Security alert for your Elmeg account
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Body
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Security Alert</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin-bottom: 20px;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fef3c7; border-radius: 6px; padding: 12px 16px;">
|
||||||
|
<p style="margin: 0; color: #92400e; font-size: 14px; font-weight: 600;">⚠️ Security Notice</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Account activity detected</h2>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
Hi {{user_name}},
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
We detected the following activity on your {{app_name}} account:
|
||||||
|
</p>
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 20px 0; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f3f4f6; border-radius: 6px; padding: 16px;">
|
||||||
|
<p style="margin: 0; color: #374151; font-size: 15px; line-height: 1.5;">
|
||||||
|
{{security_event_description}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
If this was you, no further action is needed.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
|
||||||
|
If you did not perform this action, we recommend you secure your account immediately by changing your password.
|
||||||
|
</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
||||||
|
This is an automated security notification. If you have concerns about your account security, please contact us at {{support_email}}.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
|
||||||
|
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
|
Questions? Contact us at {{support_email}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plain Text Body
|
||||||
|
|
||||||
|
```
|
||||||
|
Security alert for your Elmeg account
|
||||||
|
|
||||||
|
Hi {{user_name}},
|
||||||
|
|
||||||
|
We detected the following activity on your {{app_name}} account:
|
||||||
|
|
||||||
|
{{security_event_description}}
|
||||||
|
|
||||||
|
If this was you, no further action is needed.
|
||||||
|
|
||||||
|
If you did not perform this action, we recommend you secure your account immediately by changing your password.
|
||||||
|
|
||||||
|
---
|
||||||
|
This is an automated security notification. If you have concerns about your account security, please contact us at {{support_email}}.
|
||||||
|
|
||||||
|
{{app_name}} – The Goose Community Archive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Placeholders Reference
|
||||||
|
|
||||||
|
| Placeholder | Description | Used In |
|
||||||
|
|-------------|-------------|---------|
|
||||||
|
| `{{app_name}}` | Application name ("Elmeg") | All templates |
|
||||||
|
| `{{user_name}}` | User's display name or email | All templates |
|
||||||
|
| `{{support_email}}` | Support contact email | All templates |
|
||||||
|
| `{{verification_link}}` | Email verification URL | Email Verification |
|
||||||
|
| `{{reset_link}}` | Password reset URL | Password Reset |
|
||||||
|
| `{{security_event_description}}` | Description of the security event | Security Alert |
|
||||||
26
email/tsconfig.json
Normal file
26
email/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": [
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue