# 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: ```bash npm install nodemailer react-email @react-email/components npm install --save-dev @types/nodemailer ``` Expected: Packages installed successfully **Step 2: Verify installation** Run: ```bash npm list nodemailer react-email ``` Expected: Shows installed versions **Step 3: Add email dev script** In `package.json`, add to scripts section: ```json "email:dev": "email dev" ``` **Step 4: Commit** ```bash 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: ```javascript // 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: ```javascript // 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** ```bash npm run db:init:app ``` Expected: See "✓ Created settings table" and "✓ Created password_reset_tokens table" **Step 4: Verify tables exist** ```bash sqlite3 data/database.sqlite "SELECT name FROM sqlite_master WHERE type='table';" ``` Expected: Should include "settings" and "password_reset_tokens" **Step 5: Commit** ```bash 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: ```env # 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** ```bash 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: ```typescript /** * 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`: ```javascript 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** ```bash 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** ```typescript /** * 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** ```bash 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** ```typescript /** * 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** ```bash 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** ```typescript /** * 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** ```bash 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** ```typescript 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 ( {preview} {children} ); }; 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`: ```typescript import { Heading, Section, Text } from '@react-email/components'; import * as React from 'react'; interface EmailHeaderProps { title: string; } export const EmailHeader = ({ title }: EmailHeaderProps) => { return (
{title} Location Tracker
); }; 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`: ```typescript import { Hr, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; export const EmailFooter = () => { return ( <>
This email was sent from Location Tracker. If you have questions, please contact your administrator.
); }; 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** ```bash 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** ```typescript 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 (
Hi {username}, Welcome to Location Tracker! Your account has been created and you can now access the system. {temporaryPassword && ( <> Your temporary password is: {temporaryPassword} Please change this password after your first login for security. )} Or copy and paste this URL into your browser:{' '} {loginUrl} If you have any questions, please contact your administrator. Best regards,
Location Tracker Team
); }; 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** ```bash 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** ```typescript 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 (
Hi {username}, We received a request to reset your password for your Location Tracker account. Click the button below to reset your password: Or copy and paste this URL into your browser:{' '} {resetUrl} ⚠️ 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. For security reasons, this password reset link can only be used once. Best regards,
Location Tracker Team
); }; 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** ```bash 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** ```typescript /** * 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 { return render(WelcomeEmail(data)); } export async function renderPasswordResetEmail(data: PasswordResetEmailData): Promise { return render(PasswordResetEmail(data)); } export async function renderEmailTemplate( template: string, data: any ): Promise { switch (template) { case 'welcome': return renderWelcomeEmail(data); case 'password-reset': return renderPasswordResetEmail(data); default: throw new Error(`Unknown email template: ${template}`); } } ``` **Step 2: Commit** ```bash 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)** ```typescript /** * 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 { // 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 { 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: ```typescript /** * Send an email */ private async sendEmail( to: string, subject: string, html: string ): Promise { 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 { 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 { 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 { 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** ```bash 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** ```typescript 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** ```bash 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** ```typescript 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** ```bash 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)** ```typescript "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({ 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: ```typescript // 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: ```typescript if (loading) { return (

Loading settings...

); } return (

Settings

{/* Tab Navigation */}
{/* Status Message */} {message && (
{message.text}
)} {/* Config Source Info */}

Current source: {source === 'database' ? 'Database (Custom)' : 'Environment (.env)'}

{/* SMTP Form */}
{/* Host */}
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" />
{/* Port and Secure */}
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" />
{/* Username */}
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" />
{/* Password */}
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" />
{/* From Email */}
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" />
{/* From Name */}
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" />
{/* Reply-To */}
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" />
{/* Timeout */}
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" />
{/* Buttons */}
{source === 'database' && ( )}
{/* Test Email Modal */} {showTestModal && (

Test SMTP Connection

Enter your email address to receive a test email.

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" />
)}
); } ``` **Step 4: Commit** ```bash 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: ```typescript const navigation = [ { name: "Dashboard", href: "/admin" }, { name: "Devices", href: "/admin/devices" }, { name: "Users", href: "/admin/users" }, { name: "Settings", href: "/admin/settings" }, ]; ``` **Step 2: Commit** ```bash 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** ```typescript 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 = { 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** ```bash 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** ```typescript 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(); 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** ```bash 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** ```typescript "use client"; import { useState } from "react"; import { EMAIL_TEMPLATES, EmailTemplate } from "@/lib/types/smtp"; export default function EmailsPage() { const [selectedTemplate, setSelectedTemplate] = useState('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 (

Email Templates

{/* Status Message */} {message && (
{message.text}
)}
{/* Template List */}

Templates

{EMAIL_TEMPLATES.map((template) => ( ))}
{/* Send Test Button */}
{/* Preview */}

Preview

{EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject}