477 lines
24 KiB
Markdown
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.
|