# 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 ', 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 { // 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 ``` ## 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.