39948-vm/documentation/email-notification-service.md
2026-07-03 16:11:24 +02:00

477 lines
24 KiB
Markdown

# Email & Notification Service
Complete documentation for the Tour Builder Platform's email and notification system including Nodemailer/SES integration, verification emails, and invitations.
## Overview
The platform implements a transactional email system using Nodemailer with AWS SES (Simple Email Service) for reliable email delivery. The system handles user verification, password resets, and team invitations.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Email Service Architecture │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Email Triggers │ │
│ │ │ │
│ │ User Signup ──────────┐ │ │
│ │ Password Reset ───────┼──> AuthService ──> EmailSender ──> AWS SES │ │
│ │ User Invitation ──────┘ │ │
│ │ Email Verification ───────────────────────────────────────────────> │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Email Templates │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Address │ │ Password │ │ Invitation │ │ │
│ │ │ Verification │ │ Reset │ │ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Configuration
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `EMAIL_USER` | SMTP username (AWS SES SMTP credentials) | Yes |
| `EMAIL_PASS` | SMTP password (AWS SES SMTP credentials) | Yes |
| `EMAIL_TLS_REJECT_UNAUTHORIZED` | TLS certificate validation (`true`/`false`) | No (default: `true`) |
### Config Settings
**Source:** `backend/src/config.ts`
```javascript
email: {
from: 'Tour Builder Platform <app@flatlogic.app>',
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
auth: {
user: process.env.EMAIL_USER || '',
pass: process.env.EMAIL_PASS,
},
tls: {
rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false',
}
}
```
### AWS SES Configuration
The system uses AWS SES SMTP interface in `us-east-1` region:
- **Host:** `email-smtp.us-east-1.amazonaws.com`
- **Port:** 587 (TLS)
- **Configuration Set:** `flatlogic-app` (for tracking/analytics)
## Email Service Class
**Source:** `backend/src/services/email/index.ts`
```typescript
import nodemailer from 'nodemailer';
export default class EmailSender {
constructor(private readonly email: EmailTemplate) {}
async send(): Promise<EmailSendResult> {
// Validates: email, to, subject, html
const htmlContent = await this.email.html();
const transporter = nodemailer.createTransport(this.transportConfig);
const mailOptions = {
from: this.from,
to: this.email.to,
subject: this.email.subject,
html: htmlContent,
headers: {
'X-SES-CONFIGURATION-SET': 'flatlogic-app',
},
};
return transporter.sendMail(mailOptions);
}
static get isConfigured(): boolean {
return Boolean(config.email.auth.pass && config.email.auth.user);
}
}
```
### Configuration Check
The `EmailSender.isConfigured` static property checks if email credentials are set. When email is not configured:
- Email verification is skipped when email delivery is disabled
- Users are automatically marked as verified
- Password reset/invitation emails are not sent
## Email Types
### 1. Email Address Verification
**Source:** `backend/src/services/email/list/addressVerification.ts`
**Triggered by:**
- Manual verification request (`POST /api/auth/send-email-address-verification-email`)
**Template:** `backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html`
**Template Variables:**
| Variable | Description |
|----------|-------------|
| `{appTitle}` | Application name ("Tour Builder Platform") |
| `{signupUrl}` | Verification link with token |
> **Note:** The code attempts to replace `{to}` but this placeholder is not present in the HTML template.
**Token Generation:**
```javascript
// UsersDBApi._generateToken
const token = crypto.randomBytes(20).toString('hex');
const tokenExpiresAt = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
// Stored in users table:
// - emailVerificationToken
// - emailVerificationTokenExpiresAt
```
### 2. Password Reset
**Source:** `backend/src/services/email/list/passwordReset.ts`
**Triggered by:**
- Password reset request (`POST /api/auth/send-password-reset-email`)
**Template:** `backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html`
**Template Variables:**
| Variable | Description |
|----------|-------------|
| `{appTitle}` | Application name |
| `{resetUrl}` | Password reset link with token |
| `{accountName}` | User's email address |
**Token Generation:**
```javascript
// Stored in users table:
// - passwordResetToken
// - passwordResetTokenExpiresAt (24 hours)
```
### 3. User Invitation
**Source:** `backend/src/services/email/list/invitation.ts`
**Triggered by:**
- User creation with `sendInvitationEmails: true`
- Bulk user import
**Template:** `backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html`
**Template Variables:**
| Variable | Description |
|----------|-------------|
| `{appTitle}` | Application name |
| `{signupUrl}` | Invitation link (password reset link with `&invitation=true`) |
> **Note:** The code attempts to replace `{to}` but this placeholder is not present in the HTML template.
**Usage in UsersService:**
```javascript
// backend/src/services/users.ts
if (emailsToInvite && emailsToInvite.length) {
AuthService.sendPasswordResetEmail(email, 'invitation', host);
}
```
## HTML Template Structure
All email templates follow a consistent structure:
```html
<!DOCTYPE html>
<html>
<head>
<style>
.email-container { max-width: 600px; margin: auto; }
.email-header { background-color: #3498db; color: #fff; padding: 16px; }
.email-body { padding: 16px; }
.email-footer { background-color: #f7fafc; padding: 16px; }
.btn-primary { background-color: #3498db; color: #fff; padding: 8px 16px; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">Welcome to {appTitle}!</div>
<div class="email-body"><!-- Content --></div>
<div class="email-footer">Thanks, The {appTitle} Team</div>
</div>
</body>
</html>
```
## Notification System
### Message Lookup
**Source:** `backend/src/services/notifications/helpers.js`
The notification system uses a key-based lookup for all user-facing messages:
```javascript
const { getNotification } = require('./notifications/helpers');
// Usage
getNotification('emails.invitation.subject', appTitle);
// Returns: "You've been invited to Tour Builder Platform"
```
### Message Catalog
**Source:** `backend/src/services/notifications/list.ts`
| Key | Message |
|-----|---------|
| `app.title` | "Tour Builder Platform" |
| `emails.invitation.subject` | "You've been invited to {0}" |
| `emails.emailAddressVerification.subject` | "Verify your email for {0}" |
| `emails.passwordReset.subject` | "Reset your password for {0}" |
| `auth.userNotVerified` | "Sorry, your email has not been verified yet" |
| `auth.emailAddressVerificationEmail.invalidToken` | "Email verification link is invalid or has expired" |
| `auth.passwordReset.invalidToken` | "Password reset link is invalid or has expired" |
> **Note:** The message catalog also contains `emails.*.body` text templates, but these are not used. The system reads HTML templates from the `htmlTemplates/` directory instead.
## API Endpoints
### Email-Related Auth Endpoints
| Method | Endpoint | Description | Rate Limit |
|--------|----------|-------------|------------|
| `POST` | `/api/auth/signin/local` | User login | 10/15min |
| `POST` | `/api/auth/send-email-address-verification-email` | Resend verification (auth required) | - |
| `PUT` | `/api/auth/verify-email` | Verify email token | - |
| `POST` | `/api/auth/send-password-reset-email` | Send password reset | 5/hour |
| `PUT` | `/api/auth/password-reset` | Reset password with token | - |
| `PUT` | `/api/auth/password-update` | Change password (auth required) | - |
| `GET` | `/api/auth/email-configured` | Check if email is configured | - |
### Rate Limiting
**Source:** `backend/src/middlewares/rateLimiter.ts`
Rate limiters are imported from a centralized middleware:
```javascript
// backend/src/routes/auth.js
const {
authLimiter: signinLimiter,
passwordResetLimiter,
} = require('../middlewares/rateLimiter');
// Preconfigured limiters in rateLimiter.ts:
const authLimiter = createRateLimiter({
keyPrefix: 'auth',
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: 'Too many authentication attempts. Please try again later.',
});
// Self-registration is disabled; no signup limiter is registered.
const passwordResetLimiter = createRateLimiter({
keyPrefix: 'password-reset',
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: 'Too many password reset requests. Please try again later.',
});
```
Features:
- Uses centralized in-memory Map with automatic cleanup every 5 minutes
- Adds standard rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`)
- Returns 429 status with JSON response when exceeded
- Skips rate limiting in development for localhost
## Token Management
### User Model Fields
| Field | Type | Description |
|-------|------|-------------|
| `emailVerified` | BOOLEAN | Whether email has been verified |
| `emailVerificationToken` | TEXT | Token for email verification (40-char hex) |
| `emailVerificationTokenExpiresAt` | DATE | Token expiration timestamp |
| `passwordResetToken` | TEXT | Token for password reset (40-char hex) |
| `passwordResetTokenExpiresAt` | DATE | Token expiration timestamp |
### Token Lifecycle
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Token Lifecycle │
│ │
│ 1. Token Generated │
│ └── crypto.randomBytes(20).toString('hex') │
│ └── Expiration: Date.now() + 24 hours │
│ │
│ 2. Token Stored │
│ └── users.emailVerificationToken / passwordResetToken │
│ └── users.emailVerificationTokenExpiresAt / passwordResetTokenExpiresAt │
│ │
│ 3. Email Sent │
│ └── Link: {host}/verify-email?token={token} │
│ └── Link: {host}/password-reset?token={token} │
│ │
│ 4. Token Validated │
│ └── Check token exists │
│ └── Check tokenExpiresAt > Date.now() │
│ │
│ 5. Token Consumed │
│ └── emailVerified = true (verification) │
│ └── password updated (reset) │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Email Verification Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Email Verification Flow │
│ │
│ User Backend AWS SES │
│ │ │ │ │
│ │ POST /api/auth/send-email-address-verification-email │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Create user │ │
│ │ │ Generate verification token │ │
│ │ │ │ │
│ │ │ EmailSender.send() │ │
│ │ │──────────────────────────────>│ │
│ │ │ │ │
│ │ │<──────────────────────────────│ │
│ │<───────────────────────────│ 200 OK (JWT token) │ │
│ │ │ │ │
│ │ [Email arrives with verification link] │
│ │ │ │ │
│ │ PUT /api/auth/verify-email │ │ │
│ │ { token: "..." } │ │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Validate token & expiry │ │
│ │ │ Set emailVerified = true │ │
│ │ │ │ │
│ │<───────────────────────────│ 200 OK │ │
│ │ │ │ │
│ │ [User can now sign in normally] │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Invitation Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Invitation Flow │
│ │
│ Admin Backend New User │
│ │ │ │ │
│ │ POST /api/users │ │ │
│ │ { email, sendInvite:true } │ │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Create user record │ │
│ │ │ Generate password reset token │ │
│ │ │ Send invitation email ───────>│ │
│ │ │ │ │
│ │<───────────────────────────│ 200 OK │ │
│ │ │ │ │
│ │ │ [Email with invitation link] │
│ │ │ │ │
│ │ │ Click link: /password-reset?token= │
│ │ │<──────────────────────────────│ │
│ │ │ │ │
│ │ │ PUT /api/auth/password-reset │ │
│ │ │ { token, password } │ │
│ │ │ │ │
│ │ │ Set password │ │
│ │ │ (Auto-verifies email) │ │
│ │ │ │ │
│ │ │──────────────────────────────>│ │
│ │ │ 200 OK - Account ready │ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Error Handling
### Validation Errors
| Error Key | Message | HTTP Status |
|-----------|---------|-------------|
| `auth.userNotVerified` | "Sorry, your email has not been verified yet" | 400 |
| `auth.emailAddressVerificationEmail.invalidToken` | "Email verification link is invalid or has expired" | 400 |
| `auth.emailAddressVerificationEmail.error` | "Email not recognized" | 400 |
| `auth.passwordReset.invalidToken` | "Password reset link is invalid or has expired" | 400 |
| `auth.passwordReset.error` | "Email not recognized" | 400 |
### Graceful Degradation
When email is not configured (`EmailSender.isConfigured === false`):
1. **Signup:** Users are automatically marked as verified
2. **Signin:** Email verification check is bypassed
3. **Password Reset:** Endpoint returns success but no email is sent
```typescript
// backend/src/services/auth.ts
if (!EmailSender.isConfigured) {
user.emailVerified = true; // Auto-verify when email not configured
}
```
## File Structure
```
backend/src/
├── middlewares/
│ └── rateLimiter.ts # Centralized rate limiting middleware
└── services/
├── email/
│ ├── index.ts # EmailSender class
│ ├── list/
│ │ ├── addressVerification.ts # Email verification template
│ │ ├── invitation.ts # User invitation template
│ │ └── passwordReset.ts # Password reset template
│ └── htmlTemplates/
│ ├── addressVerification/
│ │ └── emailAddressVerification.html
│ ├── invitation/
│ │ └── invitationTemplate.html
│ └── passwordReset/
│ └── passwordResetEmail.html
└── notifications/
├── helpers.js # getNotification() function
├── list.js # Message catalog
└── errors/
├── forbidden.js # ForbiddenError class
└── validation.js # ValidationError class
```
## Known Considerations
1. **AWS SES Region:** The system is configured for `us-east-1`. For other regions, update `config.email.host`.
2. **Token Expiration:** All tokens expire after 24 hours. This was increased from the original 6 minutes to accommodate email delivery delays.
3. **Rate Limiting:** Uses centralized in-memory rate limiting in `backend/src/middlewares/rateLimiter.ts` (not distributed). Rate limits reset on server restart. Automatic cleanup of expired entries every 5 minutes.
4. **SES Configuration Set:** The `X-SES-CONFIGURATION-SET: flatlogic-app` header enables SES tracking features.
5. **Email Not Configured Mode:** The system gracefully handles missing email configuration by auto-verifying users, useful for development environments.
6. **Invitation vs Password Reset:** Invitations use the same token mechanism as password reset, with a different email template. The link includes `&invitation=true` to indicate the context.
7. **TLS Certificate Validation:** Can be disabled via `EMAIL_TLS_REJECT_UNAUTHORIZED=false` for development environments with self-signed certificates.