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

24 KiB

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

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

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:

// 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:

// 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:

// backend/src/services/users.ts
if (emailsToInvite && emailsToInvite.length) {
  AuthService.sendPasswordResetEmail(email, 'invitation', host);
}

HTML Template Structure

All email templates follow a consistent structure:

<!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:

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

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:

// 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
// 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.