Files
location-mqtt-tracker-app/docs/plans/2025-11-17-smtp-integration.md
2025-11-24 16:30:37 +00:00

86 KiB

SMTP Integration Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Implement SMTP email integration for welcome emails and password reset functionality with hybrid configuration (DB + .env fallback).

Architecture: Hybrid config approach with database-stored SMTP settings and .env fallback. Nodemailer for SMTP transport, React Email for templates. Encrypted password storage using AES-256-GCM. Admin panel UI for configuration and live email previews.

Tech Stack: Next.js 16, Nodemailer, React Email, better-sqlite3, crypto (Node.js built-in)


Phase 1: Foundation & Dependencies

Task 1: Install Dependencies

Files:

  • Modify: package.json

Step 1: Install required packages

Run:

npm install nodemailer react-email @react-email/components
npm install --save-dev @types/nodemailer

Expected: Packages installed successfully

Step 2: Verify installation

Run:

npm list nodemailer react-email

Expected: Shows installed versions

Step 3: Add email dev script

In package.json, add to scripts section:

"email:dev": "email dev"

Step 4: Commit

git add package.json package-lock.json
git commit -m "feat: add email dependencies (nodemailer, react-email)"

Task 2: Extend Database Schema

Files:

  • Modify: scripts/init-database.js:70-116

Step 1: Add settings table creation

After line 70 (after indexes creation), add:

// Create Settings table for app configuration
db.exec(`
  CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
  );
`);
console.log('✓ Created settings table');

Step 2: Add password reset tokens table

After the settings table creation, add:

// Create password reset tokens table
db.exec(`
  CREATE TABLE IF NOT EXISTS password_reset_tokens (
    token TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    used INTEGER DEFAULT 0,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
  );
`);
console.log('✓ Created password_reset_tokens table');

// Create index for performance
db.exec(`
  CREATE INDEX IF NOT EXISTS idx_reset_tokens_user_id
  ON password_reset_tokens(user_id);
`);
console.log('✓ Created password reset tokens index');

Step 3: Run database migration

npm run db:init:app

Expected: See "✓ Created settings table" and "✓ Created password_reset_tokens table"

Step 4: Verify tables exist

sqlite3 data/database.sqlite "SELECT name FROM sqlite_master WHERE type='table';"

Expected: Should include "settings" and "password_reset_tokens"

Step 5: Commit

git add scripts/init-database.js
git commit -m "feat: add settings and password_reset_tokens tables"

Task 3: Update .env.example

Files:

  • Modify: .env.example:14-18

Step 1: Update SMTP section

Replace the commented SMTP section (lines 15-18) with:

# SMTP Configuration (Fallback when DB config is empty)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM_EMAIL=noreply@example.com
SMTP_FROM_NAME=Location Tracker

# Encryption for SMTP passwords in database
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
ENCRYPTION_KEY=your-32-byte-hex-key-here

Step 2: Commit

git add .env.example
git commit -m "docs: update .env.example with SMTP and encryption settings"

Phase 2: Core Email Infrastructure

Task 4: Create Crypto Utilities

Files:

  • Create: lib/crypto-utils.ts

Step 1: Write crypto utilities

Create file with content:

/**
 * Encryption utilities for sensitive data
 * Uses AES-256-GCM for encryption
 */
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 64;

/**
 * Get encryption key from environment
 */
function getEncryptionKey(): Buffer {
  const key = process.env.ENCRYPTION_KEY;
  if (!key || key.length !== 64) {
    throw new Error('ENCRYPTION_KEY must be a 32-byte hex string (64 characters)');
  }
  return Buffer.from(key, 'hex');
}

/**
 * Encrypt text using AES-256-GCM
 * Returns base64 encoded string with format: iv:authTag:encrypted
 */
export function encrypt(text: string): string {
  try {
    const key = getEncryptionKey();
    const iv = crypto.randomBytes(IV_LENGTH);
    const cipher = crypto.createCipheriv(ALGORITHM, key, iv);

    let encrypted = cipher.update(text, 'utf8', 'base64');
    encrypted += cipher.final('base64');

    const authTag = cipher.getAuthTag();

    // Combine iv, authTag, and encrypted data
    return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
  } catch (error) {
    console.error('[Crypto] Encryption failed:', error);
    throw new Error('Failed to encrypt data');
  }
}

/**
 * Decrypt text encrypted with encrypt()
 * Expects base64 string with format: iv:authTag:encrypted
 */
export function decrypt(encryptedText: string): string {
  try {
    const key = getEncryptionKey();
    const parts = encryptedText.split(':');

    if (parts.length !== 3) {
      throw new Error('Invalid encrypted text format');
    }

    const iv = Buffer.from(parts[0], 'base64');
    const authTag = Buffer.from(parts[1], 'base64');
    const encrypted = parts[2];

    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(encrypted, 'base64', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  } catch (error) {
    console.error('[Crypto] Decryption failed:', error);
    throw new Error('Failed to decrypt data');
  }
}

/**
 * Generate a random encryption key (32 bytes as hex string)
 */
export function generateEncryptionKey(): string {
  return crypto.randomBytes(32).toString('hex');
}

Step 2: Test crypto functions

Create a test file temporarily at test-crypto.js:

const { encrypt, decrypt } = require('./lib/crypto-utils.ts');

// Set test key
process.env.ENCRYPTION_KEY = require('crypto').randomBytes(32).toString('hex');

const original = 'test-password-123';
const encrypted = encrypt(original);
const decrypted = decrypt(encrypted);

console.log('Original:', original);
console.log('Encrypted:', encrypted);
console.log('Decrypted:', decrypted);
console.log('Match:', original === decrypted);

Run: node test-crypto.js (optional - for manual verification)

Step 3: Commit

git add lib/crypto-utils.ts
git commit -m "feat: add AES-256-GCM encryption utilities for sensitive data"

Task 5: Create SMTP Configuration Types

Files:

  • Create: lib/types/smtp.ts

Step 1: Define SMTP types

/**
 * SMTP Configuration types
 */

export interface SMTPConfig {
  host: string;
  port: number;
  secure: boolean;
  auth: {
    user: string;
    pass: string; // Encrypted in DB
  };
  from: {
    email: string;
    name: string;
  };
  replyTo?: string;
  timeout?: number;
}

export interface SMTPConfigResponse {
  config: SMTPConfig | null;
  source: 'database' | 'env';
}

export interface SMTPTestRequest {
  config: SMTPConfig;
  testEmail: string;
}

export interface EmailTemplate {
  name: string;
  subject: string;
  description: string;
}

export const EMAIL_TEMPLATES: EmailTemplate[] = [
  {
    name: 'welcome',
    subject: 'Welcome to Location Tracker',
    description: 'Sent when a new user is created',
  },
  {
    name: 'password-reset',
    subject: 'Password Reset Request',
    description: 'Sent when user requests password reset',
  },
];

Step 2: Commit

git add lib/types/smtp.ts
git commit -m "feat: add SMTP configuration types"

Task 6: Create Settings Database Operations

Files:

  • Create: lib/settings-db.ts

Step 1: Write settings DB helpers

/**
 * Database operations for app settings
 */
import { getDb } from './db';
import { SMTPConfig } from './types/smtp';
import { encrypt, decrypt } from './crypto-utils';

export interface Setting {
  key: string;
  value: string;
  updated_at: string;
}

export const settingsDb = {
  /**
   * Get a setting by key
   */
  get: (key: string): Setting | null => {
    const db = getDb();
    const setting = db
      .prepare('SELECT * FROM settings WHERE key = ?')
      .get(key) as Setting | undefined;
    db.close();
    return setting || null;
  },

  /**
   * Set a setting value
   */
  set: (key: string, value: string): void => {
    const db = getDb();
    db.prepare(
      `INSERT INTO settings (key, value, updated_at)
       VALUES (?, ?, datetime('now'))
       ON CONFLICT(key) DO UPDATE SET
         value = excluded.value,
         updated_at = datetime('now')`
    ).run(key, value);
    db.close();
  },

  /**
   * Delete a setting
   */
  delete: (key: string): boolean => {
    const db = getDb();
    const result = db.prepare('DELETE FROM settings WHERE key = ?').run(key);
    db.close();
    return result.changes > 0;
  },

  /**
   * Get SMTP config from database (password decrypted)
   */
  getSMTPConfig: (): SMTPConfig | null => {
    const setting = settingsDb.get('smtp_config');
    if (!setting) return null;

    try {
      const config = JSON.parse(setting.value) as SMTPConfig;

      // Decrypt password if present
      if (config.auth?.pass) {
        config.auth.pass = decrypt(config.auth.pass);
      }

      return config;
    } catch (error) {
      console.error('[SettingsDB] Failed to parse SMTP config:', error);
      return null;
    }
  },

  /**
   * Save SMTP config to database (password encrypted)
   */
  setSMTPConfig: (config: SMTPConfig): void => {
    // Encrypt password before saving
    const configToSave = {
      ...config,
      auth: {
        ...config.auth,
        pass: encrypt(config.auth.pass),
      },
    };

    settingsDb.set('smtp_config', JSON.stringify(configToSave));
  },
};

Step 2: Commit

git add lib/settings-db.ts
git commit -m "feat: add settings database operations with SMTP config helpers"

Task 7: Create Password Reset Token Operations

Files:

  • Create: lib/password-reset-db.ts

Step 1: Write password reset DB helpers

/**
 * Database operations for password reset tokens
 */
import { getDb } from './db';
import { randomUUID } from 'crypto';

export interface PasswordResetToken {
  token: string;
  user_id: string;
  expires_at: string;
  used: number;
  created_at: string;
}

export const passwordResetDb = {
  /**
   * Create a new password reset token
   * Returns token string
   */
  create: (userId: string, expiresInHours: number = 1): string => {
    const db = getDb();
    const token = randomUUID();
    const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000).toISOString();

    db.prepare(`
      INSERT INTO password_reset_tokens (token, user_id, expires_at)
      VALUES (?, ?, ?)
    `).run(token, userId, expiresAt);

    db.close();
    return token;
  },

  /**
   * Get token by token string
   */
  findByToken: (token: string): PasswordResetToken | null => {
    const db = getDb();
    const result = db
      .prepare('SELECT * FROM password_reset_tokens WHERE token = ?')
      .get(token) as PasswordResetToken | undefined;
    db.close();
    return result || null;
  },

  /**
   * Validate token (exists, not used, not expired)
   */
  isValid: (token: string): boolean => {
    const resetToken = passwordResetDb.findByToken(token);
    if (!resetToken) return false;
    if (resetToken.used) return false;

    const now = new Date();
    const expiresAt = new Date(resetToken.expires_at);
    if (now > expiresAt) return false;

    return true;
  },

  /**
   * Mark token as used
   */
  markUsed: (token: string): boolean => {
    const db = getDb();
    const result = db
      .prepare('UPDATE password_reset_tokens SET used = 1 WHERE token = ?')
      .run(token);
    db.close();
    return result.changes > 0;
  },

  /**
   * Delete expired tokens (cleanup)
   */
  deleteExpired: (): number => {
    const db = getDb();
    const result = db
      .prepare("DELETE FROM password_reset_tokens WHERE expires_at < datetime('now')")
      .run();
    db.close();
    return result.changes;
  },

  /**
   * Delete all tokens for a user
   */
  deleteByUserId: (userId: string): number => {
    const db = getDb();
    const result = db
      .prepare('DELETE FROM password_reset_tokens WHERE user_id = ?')
      .run(userId);
    db.close();
    return result.changes;
  },
};

Step 2: Commit

git add lib/password-reset-db.ts
git commit -m "feat: add password reset token database operations"

Phase 3: React Email Templates

Task 8: Create Email Base Layout

Files:

  • Create: emails/components/email-layout.tsx

Step 1: Create base email layout

import {
  Body,
  Container,
  Head,
  Html,
  Preview,
  Section,
} from '@react-email/components';
import * as React from 'react';

interface EmailLayoutProps {
  preview: string;
  children: React.ReactNode;
}

export const EmailLayout = ({ preview, children }: EmailLayoutProps) => {
  return (
    <Html>
      <Head />
      <Preview>{preview}</Preview>
      <Body style={main}>
        <Container style={container}>{children}</Container>
      </Body>
    </Html>
  );
};

const main = {
  backgroundColor: '#f6f9fc',
  fontFamily:
    '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
  backgroundColor: '#ffffff',
  margin: '0 auto',
  padding: '20px 0 48px',
  marginBottom: '64px',
  maxWidth: '600px',
};

Step 2: Create email header component

Create emails/components/email-header.tsx:

import { Heading, Section, Text } from '@react-email/components';
import * as React from 'react';

interface EmailHeaderProps {
  title: string;
}

export const EmailHeader = ({ title }: EmailHeaderProps) => {
  return (
    <Section style={header}>
      <Heading style={h1}>{title}</Heading>
      <Text style={subtitle}>Location Tracker</Text>
    </Section>
  );
};

const header = {
  padding: '20px 40px',
  borderBottom: '1px solid #eaeaea',
};

const h1 = {
  color: '#1f2937',
  fontSize: '24px',
  fontWeight: '600',
  lineHeight: '1.3',
  margin: '0 0 8px',
};

const subtitle = {
  color: '#6b7280',
  fontSize: '14px',
  margin: '0',
};

Step 3: Create email footer component

Create emails/components/email-footer.tsx:

import { Hr, Link, Section, Text } from '@react-email/components';
import * as React from 'react';

export const EmailFooter = () => {
  return (
    <>
      <Hr style={hr} />
      <Section style={footer}>
        <Text style={footerText}>
          This email was sent from Location Tracker.
        </Text>
        <Text style={footerText}>
          If you have questions, please contact your administrator.
        </Text>
      </Section>
    </>
  );
};

const hr = {
  borderColor: '#eaeaea',
  margin: '26px 0',
};

const footer = {
  padding: '0 40px',
};

const footerText = {
  color: '#6b7280',
  fontSize: '12px',
  lineHeight: '1.5',
  margin: '0 0 8px',
};

Step 4: Commit

git add emails/components/
git commit -m "feat: add React Email base components (layout, header, footer)"

Task 9: Create Welcome Email Template

Files:

  • Create: emails/welcome.tsx

Step 1: Write welcome email template

import { Button, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailLayout } from './components/email-layout';
import { EmailHeader } from './components/email-header';
import { EmailFooter } from './components/email-footer';

interface WelcomeEmailProps {
  username: string;
  loginUrl: string;
  temporaryPassword?: string;
}

export const WelcomeEmail = ({
  username = 'user',
  loginUrl = 'http://localhost:3000/login',
  temporaryPassword,
}: WelcomeEmailProps) => {
  return (
    <EmailLayout preview="Welcome to Location Tracker">
      <EmailHeader title="Welcome!" />

      <Section style={content}>
        <Text style={paragraph}>Hi {username},</Text>

        <Text style={paragraph}>
          Welcome to Location Tracker! Your account has been created and you can now access the system.
        </Text>

        {temporaryPassword && (
          <>
            <Text style={paragraph}>
              Your temporary password is: <strong style={code}>{temporaryPassword}</strong>
            </Text>
            <Text style={paragraph}>
              Please change this password after your first login for security.
            </Text>
          </>
        )}

        <Button style={button} href={loginUrl}>
          Login to Location Tracker
        </Button>

        <Text style={paragraph}>
          Or copy and paste this URL into your browser:{' '}
          <Link href={loginUrl} style={link}>
            {loginUrl}
          </Link>
        </Text>

        <Text style={paragraph}>
          If you have any questions, please contact your administrator.
        </Text>

        <Text style={paragraph}>
          Best regards,
          <br />
          Location Tracker Team
        </Text>
      </Section>

      <EmailFooter />
    </EmailLayout>
  );
};

export default WelcomeEmail;

const content = {
  padding: '20px 40px',
};

const paragraph = {
  color: '#374151',
  fontSize: '16px',
  lineHeight: '1.6',
  margin: '0 0 16px',
};

const button = {
  backgroundColor: '#2563eb',
  borderRadius: '6px',
  color: '#fff',
  display: 'inline-block',
  fontSize: '16px',
  fontWeight: '600',
  lineHeight: '1',
  padding: '12px 24px',
  textDecoration: 'none',
  textAlign: 'center' as const,
  margin: '20px 0',
};

const link = {
  color: '#2563eb',
  textDecoration: 'underline',
};

const code = {
  backgroundColor: '#f3f4f6',
  borderRadius: '4px',
  color: '#1f2937',
  fontFamily: 'monospace',
  fontSize: '14px',
  padding: '2px 6px',
};

Step 2: Commit

git add emails/welcome.tsx
git commit -m "feat: add welcome email template"

Task 10: Create Password Reset Email Template

Files:

  • Create: emails/password-reset.tsx

Step 1: Write password reset email template

import { Button, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailLayout } from './components/email-layout';
import { EmailHeader } from './components/email-header';
import { EmailFooter } from './components/email-footer';

interface PasswordResetEmailProps {
  username: string;
  resetUrl: string;
  expiresIn?: string;
}

export const PasswordResetEmail = ({
  username = 'user',
  resetUrl = 'http://localhost:3000/reset-password?token=xxx',
  expiresIn = '1 hour',
}: PasswordResetEmailProps) => {
  return (
    <EmailLayout preview="Password Reset Request">
      <EmailHeader title="Password Reset" />

      <Section style={content}>
        <Text style={paragraph}>Hi {username},</Text>

        <Text style={paragraph}>
          We received a request to reset your password for your Location Tracker account.
        </Text>

        <Text style={paragraph}>
          Click the button below to reset your password:
        </Text>

        <Button style={button} href={resetUrl}>
          Reset Password
        </Button>

        <Text style={paragraph}>
          Or copy and paste this URL into your browser:{' '}
          <Link href={resetUrl} style={link}>
            {resetUrl}
          </Link>
        </Text>

        <Text style={warningText}>
          ⚠️ This link will expire in {expiresIn}. If you didn't request this password reset, please ignore this email or contact your administrator if you have concerns.
        </Text>

        <Text style={paragraph}>
          For security reasons, this password reset link can only be used once.
        </Text>

        <Text style={paragraph}>
          Best regards,
          <br />
          Location Tracker Team
        </Text>
      </Section>

      <EmailFooter />
    </EmailLayout>
  );
};

export default PasswordResetEmail;

const content = {
  padding: '20px 40px',
};

const paragraph = {
  color: '#374151',
  fontSize: '16px',
  lineHeight: '1.6',
  margin: '0 0 16px',
};

const button = {
  backgroundColor: '#dc2626',
  borderRadius: '6px',
  color: '#fff',
  display: 'inline-block',
  fontSize: '16px',
  fontWeight: '600',
  lineHeight: '1',
  padding: '12px 24px',
  textDecoration: 'none',
  textAlign: 'center' as const,
  margin: '20px 0',
};

const link = {
  color: '#2563eb',
  textDecoration: 'underline',
};

const warningText = {
  backgroundColor: '#fef3c7',
  border: '1px solid #fbbf24',
  borderRadius: '6px',
  color: '#92400e',
  fontSize: '14px',
  lineHeight: '1.6',
  margin: '20px 0',
  padding: '12px 16px',
};

Step 2: Commit

git add emails/password-reset.tsx
git commit -m "feat: add password reset email template"

Phase 4: Email Service

Task 11: Create Email Renderer

Files:

  • Create: lib/email-renderer.ts

Step 1: Write email renderer

/**
 * Renders React Email templates to HTML
 */
import { render } from '@react-email/components';
import WelcomeEmail from '@/emails/welcome';
import PasswordResetEmail from '@/emails/password-reset';

export interface WelcomeEmailData {
  username: string;
  loginUrl: string;
  temporaryPassword?: string;
}

export interface PasswordResetEmailData {
  username: string;
  resetUrl: string;
  expiresIn?: string;
}

export async function renderWelcomeEmail(data: WelcomeEmailData): Promise<string> {
  return render(WelcomeEmail(data));
}

export async function renderPasswordResetEmail(data: PasswordResetEmailData): Promise<string> {
  return render(PasswordResetEmail(data));
}

export async function renderEmailTemplate(
  template: string,
  data: any
): Promise<string> {
  switch (template) {
    case 'welcome':
      return renderWelcomeEmail(data);
    case 'password-reset':
      return renderPasswordResetEmail(data);
    default:
      throw new Error(`Unknown email template: ${template}`);
  }
}

Step 2: Commit

git add lib/email-renderer.ts
git commit -m "feat: add email renderer for React Email templates"

Task 12: Create Email Service

Files:

  • Create: lib/email-service.ts

Step 1: Write email service (part 1 - config)

/**
 * Email service for sending emails via SMTP
 * Supports hybrid configuration (DB + .env fallback)
 */
import nodemailer, { Transporter } from 'nodemailer';
import { SMTPConfig } from './types/smtp';
import { settingsDb } from './settings-db';
import {
  renderWelcomeEmail,
  renderPasswordResetEmail,
  WelcomeEmailData,
  PasswordResetEmailData,
} from './email-renderer';

export class EmailService {
  private transporter: Transporter | null = null;

  /**
   * Get SMTP configuration (DB first, then .env fallback)
   */
  private async getConfig(): Promise<SMTPConfig> {
    // Try database first
    const dbConfig = settingsDb.getSMTPConfig();
    if (dbConfig) {
      console.log('[EmailService] Using SMTP config from database');
      return dbConfig;
    }

    // Fallback to environment variables
    console.log('[EmailService] Using SMTP config from environment');
    const envConfig: SMTPConfig = {
      host: process.env.SMTP_HOST || '',
      port: parseInt(process.env.SMTP_PORT || '587', 10),
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER || '',
        pass: process.env.SMTP_PASS || '',
      },
      from: {
        email: process.env.SMTP_FROM_EMAIL || '',
        name: process.env.SMTP_FROM_NAME || 'Location Tracker',
      },
      timeout: 10000,
    };

    // Validate env config
    if (!envConfig.host || !envConfig.auth.user || !envConfig.auth.pass) {
      throw new Error('SMTP configuration is incomplete. Please configure SMTP settings in admin panel or .env file.');
    }

    return envConfig;
  }

  /**
   * Create and configure nodemailer transporter
   */
  private async getTransporter(): Promise<Transporter> {
    if (this.transporter) {
      return this.transporter;
    }

    const config = await this.getConfig();

    this.transporter = nodemailer.createTransport({
      host: config.host,
      port: config.port,
      secure: config.secure,
      auth: {
        user: config.auth.user,
        pass: config.auth.pass,
      },
      connectionTimeout: config.timeout || 10000,
    });

    return this.transporter;
  }

Step 2: Write email service (part 2 - send methods)

Continue in same file:

  /**
   * Send an email
   */
  private async sendEmail(
    to: string,
    subject: string,
    html: string
  ): Promise<void> {
    try {
      const config = await this.getConfig();
      const transporter = await this.getTransporter();

      const info = await transporter.sendMail({
        from: `"${config.from.name}" <${config.from.email}>`,
        to,
        subject,
        html,
        replyTo: config.replyTo,
      });

      console.log('[EmailService] Email sent:', {
        messageId: info.messageId,
        to,
        subject,
      });
    } catch (error) {
      console.error('[EmailService] Failed to send email:', error);
      throw new Error(`Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Send welcome email to new user
   */
  async sendWelcomeEmail(data: WelcomeEmailData & { email: string }): Promise<void> {
    const html = await renderWelcomeEmail({
      username: data.username,
      loginUrl: data.loginUrl,
      temporaryPassword: data.temporaryPassword,
    });

    await this.sendEmail(
      data.email,
      'Welcome to Location Tracker',
      html
    );
  }

  /**
   * Send password reset email
   */
  async sendPasswordResetEmail(data: PasswordResetEmailData & { email: string }): Promise<void> {
    const html = await renderPasswordResetEmail({
      username: data.username,
      resetUrl: data.resetUrl,
      expiresIn: data.expiresIn || '1 hour',
    });

    await this.sendEmail(
      data.email,
      'Password Reset Request - Location Tracker',
      html
    );
  }

  /**
   * Test SMTP connection
   */
  async testConnection(config?: SMTPConfig): Promise<boolean> {
    try {
      let transporter: Transporter;

      if (config) {
        // Test provided config
        transporter = nodemailer.createTransport({
          host: config.host,
          port: config.port,
          secure: config.secure,
          auth: config.auth,
          connectionTimeout: config.timeout || 10000,
        });
      } else {
        // Test current config
        transporter = await this.getTransporter();
      }

      await transporter.verify();
      console.log('[EmailService] SMTP connection test successful');
      return true;
    } catch (error) {
      console.error('[EmailService] SMTP connection test failed:', error);
      return false;
    }
  }
}

// Export singleton instance
export const emailService = new EmailService();

Step 3: Commit

git add lib/email-service.ts
git commit -m "feat: add email service with SMTP support and hybrid config"

Phase 5: Admin Panel - SMTP Settings

Task 13: Create SMTP Settings API

Files:

  • Create: app/api/admin/settings/smtp/route.ts

Step 1: Write GET and POST handlers

import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { settingsDb } from '@/lib/settings-db';
import { SMTPConfig, SMTPConfigResponse } from '@/lib/types/smtp';

/**
 * GET /api/admin/settings/smtp
 * Returns current SMTP configuration (password masked)
 */
export async function GET() {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const dbConfig = settingsDb.getSMTPConfig();

    let response: SMTPConfigResponse;

    if (dbConfig) {
      // Mask password
      const maskedConfig = {
        ...dbConfig,
        auth: {
          ...dbConfig.auth,
          pass: '***',
        },
      };
      response = { config: maskedConfig, source: 'database' };
    } else {
      // Check if env config exists
      const hasEnvConfig =
        process.env.SMTP_HOST &&
        process.env.SMTP_USER &&
        process.env.SMTP_PASS;

      if (hasEnvConfig) {
        const envConfig: SMTPConfig = {
          host: process.env.SMTP_HOST!,
          port: parseInt(process.env.SMTP_PORT || '587', 10),
          secure: process.env.SMTP_SECURE === 'true',
          auth: {
            user: process.env.SMTP_USER!,
            pass: '***',
          },
          from: {
            email: process.env.SMTP_FROM_EMAIL || '',
            name: process.env.SMTP_FROM_NAME || 'Location Tracker',
          },
          replyTo: process.env.SMTP_REPLY_TO,
          timeout: 10000,
        };
        response = { config: envConfig, source: 'env' };
      } else {
        response = { config: null, source: 'env' };
      }
    }

    return NextResponse.json(response);
  } catch (error) {
    console.error('[API] Failed to get SMTP config:', error);
    return NextResponse.json(
      { error: 'Failed to get SMTP configuration' },
      { status: 500 }
    );
  }
}

/**
 * POST /api/admin/settings/smtp
 * Save SMTP configuration to database
 */
export async function POST(request: Request) {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const body = await request.json();
    const config = body.config as SMTPConfig;

    // Validation
    if (!config.host || !config.port || !config.auth?.user || !config.auth?.pass) {
      return NextResponse.json(
        { error: 'Missing required SMTP configuration fields' },
        { status: 400 }
      );
    }

    if (config.port < 1 || config.port > 65535) {
      return NextResponse.json(
        { error: 'Port must be between 1 and 65535' },
        { status: 400 }
      );
    }

    // Save to database (password will be encrypted)
    settingsDb.setSMTPConfig(config);

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('[API] Failed to save SMTP config:', error);
    return NextResponse.json(
      { error: 'Failed to save SMTP configuration' },
      { status: 500 }
    );
  }
}

/**
 * DELETE /api/admin/settings/smtp
 * Reset to environment config
 */
export async function DELETE() {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    settingsDb.delete('smtp_config');

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('[API] Failed to delete SMTP config:', error);
    return NextResponse.json(
      { error: 'Failed to reset SMTP configuration' },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/admin/settings/smtp/route.ts
git commit -m "feat: add SMTP settings API endpoints"

Task 14: Create SMTP Test API

Files:

  • Create: app/api/admin/settings/smtp/test/route.ts

Step 1: Write test endpoint

import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { emailService } from '@/lib/email-service';
import { SMTPConfig } from '@/lib/types/smtp';

/**
 * POST /api/admin/settings/smtp/test
 * Test SMTP configuration by sending a test email
 */
export async function POST(request: Request) {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const body = await request.json();
    const { config, testEmail } = body as { config?: SMTPConfig; testEmail: string };

    if (!testEmail) {
      return NextResponse.json(
        { error: 'Test email address is required' },
        { status: 400 }
      );
    }

    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(testEmail)) {
      return NextResponse.json(
        { error: 'Invalid email address' },
        { status: 400 }
      );
    }

    // Test connection
    const connectionOk = await emailService.testConnection(config);
    if (!connectionOk) {
      return NextResponse.json(
        { error: 'SMTP connection failed. Please check your settings.' },
        { status: 500 }
      );
    }

    // Send test email
    try {
      await emailService.sendWelcomeEmail({
        email: testEmail,
        username: 'Test User',
        loginUrl: `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/login`,
        temporaryPassword: undefined,
      });

      return NextResponse.json({
        success: true,
        message: `Test email sent successfully to ${testEmail}`,
      });
    } catch (sendError) {
      console.error('[API] Test email send failed:', sendError);
      return NextResponse.json(
        {
          error: `Email send failed: ${sendError instanceof Error ? sendError.message : 'Unknown error'}`,
        },
        { status: 500 }
      );
    }
  } catch (error) {
    console.error('[API] SMTP test failed:', error);
    return NextResponse.json(
      { error: 'SMTP test failed' },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/admin/settings/smtp/test/route.ts
git commit -m "feat: add SMTP test API endpoint"

Phase 6: Admin Panel - SMTP Settings UI

Task 15: Create SMTP Settings Page

Files:

  • Create: app/admin/settings/page.tsx

Step 1: Write settings page (part 1 - state and fetch)

"use client";

import { useEffect, useState } from "react";
import { SMTPConfig, SMTPConfigResponse } from "@/lib/types/smtp";

export default function SettingsPage() {
  const [activeTab, setActiveTab] = useState<'smtp'>('smtp');
  const [config, setConfig] = useState<SMTPConfig>({
    host: '',
    port: 587,
    secure: false,
    auth: { user: '', pass: '' },
    from: { email: '', name: 'Location Tracker' },
    replyTo: '',
    timeout: 10000,
  });
  const [source, setSource] = useState<'database' | 'env'>('env');
  const [hasPassword, setHasPassword] = useState(false);
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [testing, setTesting] = useState(false);
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
  const [testEmail, setTestEmail] = useState('');
  const [showTestModal, setShowTestModal] = useState(false);

  // Fetch current config
  useEffect(() => {
    fetchConfig();
  }, []);

  const fetchConfig = async () => {
    try {
      const response = await fetch('/api/admin/settings/smtp');
      if (!response.ok) throw new Error('Failed to fetch config');

      const data: SMTPConfigResponse = await response.json();

      if (data.config) {
        setConfig(data.config);
        setHasPassword(data.config.auth.pass === '***');
      }
      setSource(data.source);
    } catch (error) {
      console.error('Failed to fetch SMTP config:', error);
      setMessage({ type: 'error', text: 'Failed to load SMTP configuration' });
    } finally {
      setLoading(false);
    }
  };

Step 2: Write settings page (part 2 - handlers)

Continue in same file:

  // Save config
  const handleSave = async (e: React.FormEvent) => {
    e.preventDefault();
    setSaving(true);
    setMessage(null);

    try {
      const response = await fetch('/api/admin/settings/smtp', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ config }),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || 'Failed to save');
      }

      setMessage({ type: 'success', text: 'SMTP settings saved successfully' });
      setHasPassword(true);
      setSource('database');

      // Clear password field for security
      setConfig({ ...config, auth: { ...config.auth, pass: '' } });
    } catch (error: any) {
      setMessage({ type: 'error', text: error.message || 'Failed to save settings' });
    } finally {
      setSaving(false);
    }
  };

  // Reset to defaults
  const handleReset = async () => {
    if (!confirm('Reset to environment defaults? This will delete database configuration.')) {
      return;
    }

    try {
      const response = await fetch('/api/admin/settings/smtp', {
        method: 'DELETE',
      });

      if (!response.ok) throw new Error('Failed to reset');

      setMessage({ type: 'success', text: 'Reset to environment defaults' });
      await fetchConfig();
    } catch (error) {
      setMessage({ type: 'error', text: 'Failed to reset settings' });
    }
  };

  // Test connection
  const handleTest = async () => {
    if (!testEmail) {
      alert('Please enter a test email address');
      return;
    }

    setTesting(true);
    setMessage(null);

    try {
      const response = await fetch('/api/admin/settings/smtp/test', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          config: hasPassword ? undefined : config,
          testEmail,
        }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Test failed');
      }

      setMessage({ type: 'success', text: data.message });
      setShowTestModal(false);
      setTestEmail('');
    } catch (error: any) {
      setMessage({ type: 'error', text: error.message || 'Connection test failed' });
    } finally {
      setTesting(false);
    }
  };

Step 3: Write settings page (part 3 - render)

Continue in same file:

  if (loading) {
    return (
      <div className="flex items-center justify-center h-64">
        <p className="text-gray-600">Loading settings...</p>
      </div>
    );
  }

  return (
    <div>
      <h2 className="text-3xl font-bold text-gray-900 mb-6">Settings</h2>

      {/* Tab Navigation */}
      <div className="border-b border-gray-200 mb-6">
        <nav className="flex gap-4">
          <button
            onClick={() => setActiveTab('smtp')}
            className={`px-4 py-2 border-b-2 font-medium ${
              activeTab === 'smtp'
                ? 'border-blue-600 text-blue-600'
                : 'border-transparent text-gray-600 hover:text-gray-900'
            }`}
          >
            SMTP Settings
          </button>
        </nav>
      </div>

      {/* Status Message */}
      {message && (
        <div
          className={`mb-6 p-4 rounded ${
            message.type === 'success'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          }`}
        >
          {message.text}
        </div>
      )}

      {/* Config Source Info */}
      <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded">
        <p className="text-sm text-blue-900">
          <strong>Current source:</strong> {source === 'database' ? 'Database (Custom)' : 'Environment (.env)'}
        </p>
      </div>

      {/* SMTP Form */}
      <form onSubmit={handleSave} className="bg-white rounded-lg shadow p-6">
        <div className="space-y-4">
          {/* Host */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              SMTP Host *
            </label>
            <input
              type="text"
              required
              value={config.host}
              onChange={(e) => setConfig({ ...config, host: e.target.value })}
              placeholder="smtp.gmail.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* Port and Secure */}
          <div className="grid grid-cols-2 gap-4">
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Port *
              </label>
              <input
                type="number"
                required
                min="1"
                max="65535"
                value={config.port}
                onChange={(e) => setConfig({ ...config, port: parseInt(e.target.value) })}
                className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              />
            </div>
            <div className="flex items-end">
              <label className="flex items-center gap-2 cursor-pointer">
                <input
                  type="checkbox"
                  checked={config.secure}
                  onChange={(e) => setConfig({ ...config, secure: e.target.checked })}
                  className="w-4 h-4 text-blue-600"
                />
                <span className="text-sm text-gray-700">Use TLS/SSL</span>
              </label>
            </div>
          </div>

          {/* Username */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Username *
            </label>
            <input
              type="text"
              required
              value={config.auth.user}
              onChange={(e) => setConfig({ ...config, auth: { ...config.auth, user: e.target.value } })}
              placeholder="your-email@example.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* Password */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Password {hasPassword && '(leave empty to keep current)'}
            </label>
            <input
              type="password"
              required={!hasPassword}
              value={config.auth.pass}
              onChange={(e) => setConfig({ ...config, auth: { ...config.auth, pass: e.target.value } })}
              placeholder={hasPassword ? '••••••••' : 'your-password'}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* From Email */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              From Email *
            </label>
            <input
              type="email"
              required
              value={config.from.email}
              onChange={(e) => setConfig({ ...config, from: { ...config.from, email: e.target.value } })}
              placeholder="noreply@example.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* From Name */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              From Name *
            </label>
            <input
              type="text"
              required
              value={config.from.name}
              onChange={(e) => setConfig({ ...config, from: { ...config.from, name: e.target.value } })}
              placeholder="Location Tracker"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* Reply-To */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Reply-To (optional)
            </label>
            <input
              type="email"
              value={config.replyTo || ''}
              onChange={(e) => setConfig({ ...config, replyTo: e.target.value })}
              placeholder="support@example.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          {/* Timeout */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Timeout (ms)
            </label>
            <input
              type="number"
              min="1000"
              value={config.timeout}
              onChange={(e) => setConfig({ ...config, timeout: parseInt(e.target.value) })}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
        </div>

        {/* Buttons */}
        <div className="flex gap-3 mt-6">
          <button
            type="submit"
            disabled={saving}
            className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400"
          >
            {saving ? 'Saving...' : 'Save Settings'}
          </button>
          <button
            type="button"
            onClick={() => setShowTestModal(true)}
            className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
          >
            Test Connection
          </button>
          {source === 'database' && (
            <button
              type="button"
              onClick={handleReset}
              className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100"
            >
              Reset to Defaults
            </button>
          )}
        </div>
      </form>

      {/* Test Email Modal */}
      {showTestModal && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
          <div className="bg-white rounded-lg p-6 w-full max-w-md">
            <h3 className="text-xl font-bold mb-4">Test SMTP Connection</h3>
            <p className="text-sm text-gray-600 mb-4">
              Enter your email address to receive a test email.
            </p>
            <input
              type="email"
              value={testEmail}
              onChange={(e) => setTestEmail(e.target.value)}
              placeholder="your-email@example.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4"
            />
            <div className="flex gap-3">
              <button
                onClick={() => {
                  setShowTestModal(false);
                  setTestEmail('');
                }}
                className="flex-1 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100"
              >
                Cancel
              </button>
              <button
                onClick={handleTest}
                disabled={testing || !testEmail}
                className="flex-1 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400"
              >
                {testing ? 'Sending...' : 'Send Test Email'}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Step 4: Commit

git add app/admin/settings/page.tsx
git commit -m "feat: add SMTP settings UI page"

Task 16: Update Admin Navigation

Files:

  • Modify: app/admin/layout.tsx:14-18

Step 1: Add Settings link to navigation

In the navigation array, add:

const navigation = [
  { name: "Dashboard", href: "/admin" },
  { name: "Devices", href: "/admin/devices" },
  { name: "Users", href: "/admin/users" },
  { name: "Settings", href: "/admin/settings" },
];

Step 2: Commit

git add app/admin/layout.tsx
git commit -m "feat: add Settings to admin navigation"

Phase 7: Email Preview & Testing

Task 17: Create Email Preview API

Files:

  • Create: app/api/admin/emails/preview/route.ts

Step 1: Write preview endpoint

import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { renderEmailTemplate } from '@/lib/email-renderer';

/**
 * GET /api/admin/emails/preview?template=welcome
 * Render email template with sample data for preview
 */
export async function GET(request: Request) {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const { searchParams } = new URL(request.url);
    const template = searchParams.get('template');

    if (!template) {
      return NextResponse.json(
        { error: 'Template parameter is required' },
        { status: 400 }
      );
    }

    // Sample data for each template
    const sampleData: Record<string, any> = {
      welcome: {
        username: 'John Doe',
        loginUrl: `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/login`,
        temporaryPassword: 'TempPass123!',
      },
      'password-reset': {
        username: 'John Doe',
        resetUrl: `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/reset-password?token=sample-token-123`,
        expiresIn: '1 hour',
      },
    };

    if (!sampleData[template]) {
      return NextResponse.json(
        { error: `Unknown template: ${template}` },
        { status: 400 }
      );
    }

    const html = await renderEmailTemplate(template, sampleData[template]);

    return new NextResponse(html, {
      headers: { 'Content-Type': 'text/html' },
    });
  } catch (error) {
    console.error('[API] Email preview failed:', error);
    return NextResponse.json(
      { error: 'Failed to render email template' },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/admin/emails/preview/route.ts
git commit -m "feat: add email preview API endpoint"

Task 18: Create Send Test Email API

Files:

  • Create: app/api/admin/emails/send-test/route.ts

Step 1: Write send test endpoint

import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { emailService } from '@/lib/email-service';

// Simple rate limiting (in-memory)
const rateLimitMap = new Map<string, number[]>();
const RATE_LIMIT = 5; // max requests
const RATE_WINDOW = 60 * 1000; // per minute

function checkRateLimit(ip: string): boolean {
  const now = Date.now();
  const requests = rateLimitMap.get(ip) || [];

  // Filter out old requests
  const recentRequests = requests.filter(time => now - time < RATE_WINDOW);

  if (recentRequests.length >= RATE_LIMIT) {
    return false;
  }

  recentRequests.push(now);
  rateLimitMap.set(ip, recentRequests);
  return true;
}

/**
 * POST /api/admin/emails/send-test
 * Send test email with specific template
 */
export async function POST(request: Request) {
  try {
    const session = await auth();

    if (!session?.user || (session.user as any).role !== 'ADMIN') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // Rate limiting
    const ip = request.headers.get('x-forwarded-for') || 'unknown';
    if (!checkRateLimit(ip)) {
      return NextResponse.json(
        { error: 'Too many requests. Please wait a minute.' },
        { status: 429 }
      );
    }

    const body = await request.json();
    const { template, email } = body;

    if (!template || !email) {
      return NextResponse.json(
        { error: 'Template and email are required' },
        { status: 400 }
      );
    }

    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      return NextResponse.json(
        { error: 'Invalid email address' },
        { status: 400 }
      );
    }

    const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';

    // Send appropriate template
    switch (template) {
      case 'welcome':
        await emailService.sendWelcomeEmail({
          email,
          username: 'Test User',
          loginUrl: `${baseUrl}/login`,
          temporaryPassword: 'TempPass123!',
        });
        break;

      case 'password-reset':
        await emailService.sendPasswordResetEmail({
          email,
          username: 'Test User',
          resetUrl: `${baseUrl}/reset-password?token=sample-token-123`,
          expiresIn: '1 hour',
        });
        break;

      default:
        return NextResponse.json(
          { error: `Unknown template: ${template}` },
          { status: 400 }
        );
    }

    return NextResponse.json({
      success: true,
      message: `Test email sent to ${email}`,
    });
  } catch (error) {
    console.error('[API] Send test email failed:', error);
    return NextResponse.json(
      { error: `Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}` },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/admin/emails/send-test/route.ts
git commit -m "feat: add send test email API with rate limiting"

Task 19: Create Email Preview Page

Files:

  • Create: app/admin/emails/page.tsx

Step 1: Write emails preview page

"use client";

import { useState } from "react";
import { EMAIL_TEMPLATES, EmailTemplate } from "@/lib/types/smtp";

export default function EmailsPage() {
  const [selectedTemplate, setSelectedTemplate] = useState<string>('welcome');
  const [testEmail, setTestEmail] = useState('');
  const [sending, setSending] = useState(false);
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
  const [showSendModal, setShowSendModal] = useState(false);

  const handleSendTest = async () => {
    if (!testEmail) {
      alert('Please enter a test email address');
      return;
    }

    setSending(true);
    setMessage(null);

    try {
      const response = await fetch('/api/admin/emails/send-test', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          template: selectedTemplate,
          email: testEmail,
        }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to send');
      }

      setMessage({ type: 'success', text: data.message });
      setShowSendModal(false);
      setTestEmail('');
    } catch (error: any) {
      setMessage({ type: 'error', text: error.message || 'Failed to send test email' });
    } finally {
      setSending(false);
    }
  };

  const previewUrl = `/api/admin/emails/preview?template=${selectedTemplate}`;

  return (
    <div>
      <h2 className="text-3xl font-bold text-gray-900 mb-6">Email Templates</h2>

      {/* Status Message */}
      {message && (
        <div
          className={`mb-6 p-4 rounded ${
            message.type === 'success'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          }`}
        >
          {message.text}
        </div>
      )}

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Template List */}
        <div className="lg:col-span-1">
          <div className="bg-white rounded-lg shadow">
            <div className="px-6 py-4 border-b border-gray-200">
              <h3 className="text-lg font-semibold text-gray-900">Templates</h3>
            </div>
            <div className="p-4">
              <div className="space-y-2">
                {EMAIL_TEMPLATES.map((template) => (
                  <button
                    key={template.name}
                    onClick={() => setSelectedTemplate(template.name)}
                    className={`w-full text-left px-4 py-3 rounded-md transition-colors ${
                      selectedTemplate === template.name
                        ? 'bg-blue-600 text-white'
                        : 'bg-gray-50 hover:bg-gray-100 text-gray-900'
                    }`}
                  >
                    <p className="font-medium">{template.subject}</p>
                    <p className={`text-sm mt-1 ${
                      selectedTemplate === template.name
                        ? 'text-blue-100'
                        : 'text-gray-600'
                    }`}>
                      {template.description}
                    </p>
                  </button>
                ))}
              </div>
            </div>
          </div>

          {/* Send Test Button */}
          <button
            onClick={() => setShowSendModal(true)}
            className="w-full mt-4 px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium"
          >
            Send Test Email
          </button>
        </div>

        {/* Preview */}
        <div className="lg:col-span-2">
          <div className="bg-white rounded-lg shadow">
            <div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
              <h3 className="text-lg font-semibold text-gray-900">Preview</h3>
              <span className="text-sm text-gray-600">
                {EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject}
              </span>
            </div>
            <div className="p-4">
              <iframe
                src={previewUrl}
                className="w-full border border-gray-300 rounded"
                style={{ height: '600px' }}
                title="Email Preview"
              />
            </div>
          </div>
        </div>
      </div>

      {/* Send Test Email Modal */}
      {showSendModal && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
          <div className="bg-white rounded-lg p-6 w-full max-w-md">
            <h3 className="text-xl font-bold mb-4">Send Test Email</h3>
            <p className="text-sm text-gray-600 mb-2">
              Template: <strong>{EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject}</strong>
            </p>
            <p className="text-sm text-gray-600 mb-4">
              Enter your email address to receive a test email.
            </p>
            <input
              type="email"
              value={testEmail}
              onChange={(e) => setTestEmail(e.target.value)}
              placeholder="your-email@example.com"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4"
            />
            <div className="flex gap-3">
              <button
                onClick={() => {
                  setShowSendModal(false);
                  setTestEmail('');
                }}
                className="flex-1 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100"
              >
                Cancel
              </button>
              <button
                onClick={handleSendTest}
                disabled={sending || !testEmail}
                className="flex-1 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400"
              >
                {sending ? 'Sending...' : 'Send Test'}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Step 2: Add Emails link to navigation

In app/admin/layout.tsx, update navigation:

const navigation = [
  { name: "Dashboard", href: "/admin" },
  { name: "Devices", href: "/admin/devices" },
  { name: "Users", href: "/admin/users" },
  { name: "Settings", href: "/admin/settings" },
  { name: "Emails", href: "/admin/emails" },
];

Step 3: Commit

git add app/admin/emails/page.tsx app/admin/layout.tsx
git commit -m "feat: add email preview page with send test functionality"

Phase 8: Password Reset Flow

Task 20: Create Forgot Password API

Files:

  • Create: app/api/auth/forgot-password/route.ts

Step 1: Write forgot password endpoint

import { NextResponse } from 'next/server';
import { userDb } from '@/lib/db';
import { passwordResetDb } from '@/lib/password-reset-db';
import { emailService } from '@/lib/email-service';

/**
 * POST /api/auth/forgot-password
 * Request password reset email
 */
export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { email } = body;

    if (!email) {
      return NextResponse.json(
        { error: 'Email is required' },
        { status: 400 }
      );
    }

    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      return NextResponse.json(
        { error: 'Invalid email address' },
        { status: 400 }
      );
    }

    // Find user by email
    const users = userDb.findAll();
    const user = users.find(u => u.email?.toLowerCase() === email.toLowerCase());

    // SECURITY: Always return success to prevent user enumeration
    // Even if user doesn't exist, return success but don't send email
    if (!user) {
      console.log('[ForgotPassword] Email not found, but returning success (security)');
      return NextResponse.json({
        success: true,
        message: 'If an account exists with this email, a password reset link has been sent.',
      });
    }

    // Create password reset token
    const token = passwordResetDb.create(user.id, 1); // 1 hour expiry

    // Send password reset email
    const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
    const resetUrl = `${baseUrl}/reset-password?token=${token}`;

    try {
      await emailService.sendPasswordResetEmail({
        email: user.email!,
        username: user.username,
        resetUrl,
        expiresIn: '1 hour',
      });

      console.log('[ForgotPassword] Password reset email sent to:', user.email);
    } catch (emailError) {
      console.error('[ForgotPassword] Failed to send email:', emailError);
      // Don't fail the request if email fails - log and continue
    }

    return NextResponse.json({
      success: true,
      message: 'If an account exists with this email, a password reset link has been sent.',
    });
  } catch (error) {
    console.error('[ForgotPassword] Error:', error);
    return NextResponse.json(
      { error: 'An error occurred. Please try again later.' },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/auth/forgot-password/route.ts
git commit -m "feat: add forgot password API endpoint"

Task 21: Create Reset Password API

Files:

  • Create: app/api/auth/reset-password/route.ts

Step 1: Write reset password endpoint

import { NextResponse } from 'next/server';
import { userDb } from '@/lib/db';
import { passwordResetDb } from '@/lib/password-reset-db';
import bcrypt from 'bcryptjs';

/**
 * POST /api/auth/reset-password
 * Reset password with token
 */
export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { token, newPassword } = body;

    if (!token || !newPassword) {
      return NextResponse.json(
        { error: 'Token and new password are required' },
        { status: 400 }
      );
    }

    // Password validation
    if (newPassword.length < 6) {
      return NextResponse.json(
        { error: 'Password must be at least 6 characters' },
        { status: 400 }
      );
    }

    // Validate token
    if (!passwordResetDb.isValid(token)) {
      return NextResponse.json(
        { error: 'Invalid or expired reset token' },
        { status: 400 }
      );
    }

    // Get token details
    const resetToken = passwordResetDb.findByToken(token);
    if (!resetToken) {
      return NextResponse.json(
        { error: 'Invalid reset token' },
        { status: 400 }
      );
    }

    // Get user
    const user = userDb.findById(resetToken.user_id);
    if (!user) {
      return NextResponse.json(
        { error: 'User not found' },
        { status: 404 }
      );
    }

    // Hash new password
    const passwordHash = await bcrypt.hash(newPassword, 10);

    // Update user password
    userDb.update(user.id, { passwordHash });

    // Mark token as used
    passwordResetDb.markUsed(token);

    console.log('[ResetPassword] Password reset successful for user:', user.username);

    return NextResponse.json({
      success: true,
      message: 'Password has been reset successfully',
    });
  } catch (error) {
    console.error('[ResetPassword] Error:', error);
    return NextResponse.json(
      { error: 'Failed to reset password' },
      { status: 500 }
    );
  }
}

/**
 * GET /api/auth/reset-password?token=xxx
 * Validate reset token (for checking if link is still valid)
 */
export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const token = searchParams.get('token');

    if (!token) {
      return NextResponse.json(
        { error: 'Token is required' },
        { status: 400 }
      );
    }

    const isValid = passwordResetDb.isValid(token);

    return NextResponse.json({ valid: isValid });
  } catch (error) {
    console.error('[ResetPassword] Validation error:', error);
    return NextResponse.json(
      { error: 'Failed to validate token' },
      { status: 500 }
    );
  }
}

Step 2: Commit

git add app/api/auth/reset-password/route.ts
git commit -m "feat: add reset password API endpoint"

Task 22: Create Forgot Password Page

Files:

  • Create: app/forgot-password/page.tsx

Step 1: Write forgot password page

"use client";

import { useState } from "react";
import Link from "next/link";

export default function ForgotPasswordPage() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [submitted, setSubmitted] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/forgot-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to send reset email');
      }

      setSubmitted(true);
    } catch (err: any) {
      setError(err.message || 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  if (submitted) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
        <div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
          <div className="text-center">
            <div className="text-5xl mb-4"></div>
            <h2 className="text-2xl font-bold text-gray-900 mb-2">
              Check Your Email
            </h2>
            <p className="text-gray-600 mb-6">
              If an account exists with the email <strong>{email}</strong>, you will receive a password reset link shortly.
            </p>
            <Link
              href="/login"
              className="text-blue-600 hover:text-blue-700 font-medium"
            >
               Back to Login
            </Link>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
      <div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
        <h2 className="text-2xl font-bold text-gray-900 mb-2">
          Forgot Password
        </h2>
        <p className="text-gray-600 mb-6">
          Enter your email address and we'll send you a link to reset your password.
        </p>

        {error && (
          <div className="mb-4 p-3 bg-red-100 text-red-800 rounded">
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit}>
          <div className="mb-6">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Email Address
            </label>
            <input
              type="email"
              required
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="your-email@example.com"
            />
          </div>

          <button
            type="submit"
            disabled={loading}
            className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 font-medium"
          >
            {loading ? 'Sending...' : 'Send Reset Link'}
          </button>

          <div className="mt-4 text-center">
            <Link
              href="/login"
              className="text-blue-600 hover:text-blue-700 text-sm"
            >
               Back to Login
            </Link>
          </div>
        </form>
      </div>
    </div>
  );
}

Step 2: Commit

git add app/forgot-password/page.tsx
git commit -m "feat: add forgot password page"

Task 23: Create Reset Password Page

Files:

  • Create: app/reset-password/page.tsx

Step 1: Write reset password page

"use client";

import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";

export default function ResetPasswordPage() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const token = searchParams.get('token');

  const [newPassword, setNewPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [validating, setValidating] = useState(true);
  const [tokenValid, setTokenValid] = useState(false);
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);

  // Validate token on mount
  useEffect(() => {
    if (!token) {
      setError('Invalid reset link');
      setValidating(false);
      return;
    }

    const validateToken = async () => {
      try {
        const response = await fetch(`/api/auth/reset-password?token=${token}`);
        const data = await response.json();

        if (data.valid) {
          setTokenValid(true);
        } else {
          setError('This reset link is invalid or has expired');
        }
      } catch (err) {
        setError('Failed to validate reset link');
      } finally {
        setValidating(false);
      }
    };

    validateToken();
  }, [token]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    if (newPassword !== confirmPassword) {
      setError('Passwords do not match');
      return;
    }

    if (newPassword.length < 6) {
      setError('Password must be at least 6 characters');
      return;
    }

    setLoading(true);

    try {
      const response = await fetch('/api/auth/reset-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token, newPassword }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to reset password');
      }

      setSuccess(true);

      // Redirect to login after 3 seconds
      setTimeout(() => {
        router.push('/login');
      }, 3000);
    } catch (err: any) {
      setError(err.message || 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  if (validating) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100">
        <p className="text-gray-600">Validating reset link...</p>
      </div>
    );
  }

  if (!tokenValid) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
        <div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
          <div className="text-center">
            <div className="text-5xl mb-4"></div>
            <h2 className="text-2xl font-bold text-gray-900 mb-2">
              Invalid Reset Link
            </h2>
            <p className="text-gray-600 mb-6">
              {error || 'This password reset link is invalid or has expired.'}
            </p>
            <Link
              href="/forgot-password"
              className="text-blue-600 hover:text-blue-700 font-medium"
            >
              Request New Reset Link 
            </Link>
          </div>
        </div>
      </div>
    );
  }

  if (success) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
        <div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
          <div className="text-center">
            <div className="text-5xl mb-4"></div>
            <h2 className="text-2xl font-bold text-gray-900 mb-2">
              Password Reset Successful
            </h2>
            <p className="text-gray-600 mb-6">
              Your password has been reset successfully. Redirecting to login...
            </p>
            <Link
              href="/login"
              className="text-blue-600 hover:text-blue-700 font-medium"
            >
              Go to Login 
            </Link>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
      <div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
        <h2 className="text-2xl font-bold text-gray-900 mb-2">
          Reset Password
        </h2>
        <p className="text-gray-600 mb-6">
          Enter your new password below.
        </p>

        {error && (
          <div className="mb-4 p-3 bg-red-100 text-red-800 rounded">
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              New Password
            </label>
            <input
              type="password"
              required
              minLength={6}
              value={newPassword}
              onChange={(e) => setNewPassword(e.target.value)}
              className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="At least 6 characters"
            />
          </div>

          <div className="mb-6">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Confirm New Password
            </label>
            <input
              type="password"
              required
              minLength={6}
              value={confirmPassword}
              onChange={(e) => setConfirmPassword(e.target.value)}
              className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="Re-enter password"
            />
          </div>

          <button
            type="submit"
            disabled={loading}
            className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 font-medium"
          >
            {loading ? 'Resetting...' : 'Reset Password'}
          </button>
        </form>
      </div>
    </div>
  );
}

Step 2: Commit

git add app/reset-password/page.tsx
git commit -m "feat: add reset password page"

Task 24: Update Login Page

Files:

  • Modify: app/login/page.tsx

Step 1: Add forgot password link

Find the login form and add a "Forgot Password?" link below the password field. Look for the password input section and add after it:

{/* After password input field, before submit button */}
<div className="text-right mb-4">
  <Link
    href="/forgot-password"
    className="text-sm text-blue-600 hover:text-blue-700"
  >
    Forgot Password?
  </Link>
</div>

Don't forget to add the import at the top:

import Link from "next/link";

Step 2: Commit

git add app/login/page.tsx
git commit -m "feat: add forgot password link to login page"

Phase 9: Integration with User Management

Task 25: Integrate Welcome Email in User Creation

Files:

  • Modify: app/api/users/route.ts:78-92

Step 1: Import email service

At the top of the file, add import:

import { emailService } from '@/lib/email-service';

Step 2: Send welcome email after user creation

After the user is created (around line 87-92), add email sending:

// Create user
const user = userDb.create({
  id: randomUUID(),
  username,
  email: email || null,
  passwordHash,
  role: role || 'VIEWER',
});

// Send welcome email (don't fail if email fails)
if (email) {
  try {
    const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
    await emailService.sendWelcomeEmail({
      email,
      username,
      loginUrl: `${baseUrl}/login`,
      temporaryPassword: password, // Send the original password
    });
    console.log('[UserCreate] Welcome email sent to:', email);
  } catch (emailError) {
    console.error('[UserCreate] Failed to send welcome email:', emailError);
    // Don't fail user creation if email fails
  }
}

// Remove password hash from response
const { passwordHash: _, ...safeUser } = user;

Step 3: Commit

git add app/api/users/route.ts
git commit -m "feat: integrate welcome email in user creation"

Task 26: Add Manual Email Actions to Users Page

Files:

  • Modify: app/admin/users/page.tsx

Step 1: Add resend welcome email function

After the handleDelete function (around line 129), add:

// Resend welcome email
const handleResendWelcome = async (user: User) => {
  if (!user.email) {
    alert('This user has no email address');
    return;
  }

  if (!confirm(`Send welcome email to ${user.email}?`)) {
    return;
  }

  try {
    const response = await fetch('/api/admin/emails/send-test', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        template: 'welcome',
        email: user.email,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Failed to send email');
    }

    alert('Welcome email sent successfully');
  } catch (err: any) {
    alert(err.message || 'Failed to send welcome email');
  }
};

// Send password reset
const handleSendPasswordReset = async (user: User) => {
  if (!user.email) {
    alert('This user has no email address');
    return;
  }

  if (!confirm(`Send password reset email to ${user.email}?`)) {
    return;
  }

  try {
    const response = await fetch('/api/auth/forgot-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: user.email }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Failed to send email');
    }

    alert('Password reset email sent successfully');
  } catch (err: any) {
    alert(err.message || 'Failed to send password reset email');
  }
};

Step 2: Add email action buttons to user cards

Find the buttons section in the user card (around line 222-235) and update:

<div className="flex gap-2">
  <button
    onClick={() => openEditModal(user)}
    className="flex-1 px-3 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
  >
    Edit
  </button>
  <button
    onClick={() => openDeleteModal(user)}
    className="flex-1 px-3 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700"
  >
    Delete
  </button>
</div>

{/* Email Actions */}
{user.email && (
  <div className="flex gap-2 mt-2">
    <button
      onClick={() => handleResendWelcome(user)}
      className="flex-1 px-3 py-2 bg-green-600 text-white text-xs rounded-md hover:bg-green-700"
    >
      📧 Resend Welcome
    </button>
    <button
      onClick={() => handleSendPasswordReset(user)}
      className="flex-1 px-3 py-2 bg-orange-600 text-white text-xs rounded-md hover:bg-orange-700"
    >
      🔑 Reset Password
    </button>
  </div>
)}

Step 3: Commit

git add app/admin/users/page.tsx
git commit -m "feat: add email action buttons to user management"

Phase 10: Testing & Documentation

Task 27: Create Test Script

Files:

  • Create: scripts/test-smtp.js

Step 1: Write test script

#!/usr/bin/env node
/**
 * Test SMTP configuration and email sending
 * Usage: node scripts/test-smtp.js your-email@example.com
 */

require('dotenv').config({ path: '.env.local' });
const { emailService } = require('../lib/email-service.ts');

const testEmail = process.argv[2];

if (!testEmail) {
  console.error('Usage: node scripts/test-smtp.js your-email@example.com');
  process.exit(1);
}

async function testSMTP() {
  console.log('Testing SMTP configuration...\n');

  try {
    // Test connection
    console.log('1. Testing SMTP connection...');
    const connected = await emailService.testConnection();
    if (connected) {
      console.log('✓ SMTP connection successful\n');
    } else {
      console.error('✗ SMTP connection failed\n');
      process.exit(1);
    }

    // Test welcome email
    console.log('2. Sending test welcome email...');
    await emailService.sendWelcomeEmail({
      email: testEmail,
      username: 'Test User',
      loginUrl: 'http://localhost:3000/login',
      temporaryPassword: 'TempPass123!',
    });
    console.log('✓ Welcome email sent\n');

    // Test password reset email
    console.log('3. Sending test password reset email...');
    await emailService.sendPasswordResetEmail({
      email: testEmail,
      username: 'Test User',
      resetUrl: 'http://localhost:3000/reset-password?token=test-token-123',
      expiresIn: '1 hour',
    });
    console.log('✓ Password reset email sent\n');

    console.log('All tests passed! Check your inbox at:', testEmail);
  } catch (error) {
    console.error('Test failed:', error.message);
    process.exit(1);
  }
}

testSMTP();

Step 2: Make script executable

chmod +x scripts/test-smtp.js

Step 3: Commit

git add scripts/test-smtp.js
git commit -m "feat: add SMTP test script"

Task 28: Update Documentation

Files:

  • Create: docs/SMTP-SETUP.md

Step 1: Write SMTP setup guide

# SMTP Setup Guide

## Overview

This guide explains how to configure SMTP for email functionality in the Location Tracker app.

## Prerequisites

- SMTP server credentials (Gmail, SendGrid, Mailgun, etc.)
- For Gmail: App Password (not regular password)

## Configuration Methods

### Method 1: Environment Variables (Fallback)

1. Copy `.env.example` to `.env.local`:
   ```bash
   cp .env.example .env.local
  1. Generate encryption key:

    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    
  2. Update SMTP settings in .env.local:

    SMTP_HOST=smtp.gmail.com
    SMTP_PORT=587
    SMTP_SECURE=false
    SMTP_USER=your-email@gmail.com
    SMTP_PASS=your-app-password
    SMTP_FROM_EMAIL=noreply@example.com
    SMTP_FROM_NAME=Location Tracker
    ENCRYPTION_KEY=<generated-key-from-step-2>
    
  1. Log in as admin
  2. Navigate to SettingsSMTP Settings
  3. Fill in SMTP configuration
  4. Click Test Connection to verify
  5. Click Save Settings

Provider-Specific Setup

Gmail

  1. Enable 2-Factor Authentication
  2. Generate App Password:
    • Go to Google Account → Security → 2-Step Verification → App Passwords
    • Select "Mail" and generate password
  3. Use generated password in SMTP_PASS

Settings:

  • Host: smtp.gmail.com
  • Port: 587
  • Secure: false (uses STARTTLS)

SendGrid

Settings:

  • Host: smtp.sendgrid.net
  • Port: 587
  • Secure: false
  • User: apikey
  • Pass: Your SendGrid API key

Mailgun

Settings:

  • Host: smtp.mailgun.org
  • Port: 587
  • Secure: false
  • User: Your Mailgun SMTP username
  • Pass: Your Mailgun SMTP password

Testing

Via Script

node scripts/test-smtp.js your-email@example.com

Via Admin Panel

  1. Go to Emails page
  2. Select a template
  3. Click Send Test Email
  4. Enter your email and send

Troubleshooting

Connection Timeout

  • Check firewall settings
  • Verify port is correct (587 for STARTTLS, 465 for SSL)
  • Try toggling "Use TLS/SSL" setting

Authentication Failed

  • Verify username and password
  • For Gmail: Use App Password, not account password
  • Check if SMTP is enabled for your account

Emails Not Received

  • Check spam/junk folder
  • Verify "From Email" is valid
  • Check provider sending limits

Email Templates

Available templates:

  • Welcome Email: Sent when new user is created
  • Password Reset: Sent when user requests password reset

Templates can be previewed in Admin → Emails.

Security Notes

  • Passwords stored in database are encrypted using AES-256-GCM
  • ENCRYPTION_KEY must be kept secret
  • Never commit .env.local to git
  • Use environment-specific SMTP credentials

**Step 2: Commit**

```bash
git add docs/SMTP-SETUP.md
git commit -m "docs: add SMTP setup guide"

Task 29: Final Integration Test

Files:

  • N/A (Manual testing)

Step 1: Generate encryption key

Run:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Copy output to .env.local as ENCRYPTION_KEY

Step 2: Configure SMTP in admin panel

  1. Start dev server: npm run dev
  2. Login as admin
  3. Go to /admin/settings
  4. Enter SMTP settings
  5. Click "Test Connection"
  6. If successful, click "Save Settings"

Step 3: Test welcome email

  1. Go to /admin/users
  2. Create new user with email address
  3. Check email inbox for welcome message

Step 4: Test password reset

  1. Logout
  2. Go to /forgot-password
  3. Enter email address
  4. Check inbox for reset email
  5. Click reset link
  6. Enter new password
  7. Login with new password

Step 5: Test email templates

  1. Go to /admin/emails
  2. Preview each template
  3. Send test email for each template
  4. Verify emails received

Step 6: Final commit

git add -A
git commit -m "feat: SMTP integration complete and tested"

Completion Checklist

  • All dependencies installed
  • Database schema extended
  • Crypto utilities working
  • Email templates render correctly
  • SMTP settings configurable via admin panel
  • SMTP test connection works
  • Welcome emails sent on user creation
  • Password reset flow complete
  • Email preview page functional
  • Forgot password page works
  • Reset password page works
  • Login page has forgot password link
  • User management has email actions
  • Documentation complete
  • All tests pass

Notes

  • ENCRYPTION_KEY: Must be 32-byte hex string (64 characters)
  • Password Storage: Encrypted in DB, never logged
  • Email Failures: Don't fail user creation if email fails
  • Rate Limiting: 5 test emails per minute
  • Token Expiry: Password reset tokens expire after 1 hour
  • Security: Always return success on forgot password (prevent user enumeration)

Future Enhancements

  • Email queue for bulk sending
  • Email templates editor in admin panel
  • Email delivery tracking
  • Multiple SMTP providers with failover
  • Scheduled email reports
  • Email preferences per user