Mail Service & Magic Link Authentication — Design Spec
DerinDate: 2026-04-01 Status: Approved Scope: Global enterprise mail service + Admin magic link login
1. Overview
Bölüm başlığı “1. Overview”Build a global, enterprise-grade email service for ECU Tuning Portal using React Email (component-based HTML templates) + Nodemailer (SMTP transport via Plesk). The first consumer is Admin Magic Link Authentication — a passwordless login option alongside the existing password + 2FA flow.
- Reusable mail service that any part of the app (admin, customer, landing) can consume
- Branded, i18n-ready email templates matching the portal’s dark/red tech aesthetic
- Magic link login for admin users — hybrid with existing password/2FA auth
- Enterprise-grade security: single-use tokens, TTL, rate limiting, email enumeration protection
Non-Goals
Bölüm başlığı “Non-Goals”- Customer portal magic link (future phase)
- Email queue/worker system (not needed at current scale)
- Third-party email providers (Resend, SendGrid) — using existing Plesk SMTP
2. Architecture
Bölüm başlığı “2. Architecture”2.1 Directory Structure
Bölüm başlığı “2.1 Directory Structure”lib/mail/├── transport.ts # Nodemailer SMTP connection (singleton, pooled)├── service.ts # Main mail service — sendMail(options)├── tokens.ts # Magic link token generate/verify (crypto + DB)├── i18n.ts # Template translation dictionaries└── templates/ ├── base-layout.tsx # Shared layout (logo, footer, brand colors) ├── magic-link.tsx # Magic link login template └── index.ts # Template registry (type-safe exports)2.2 Layers & Responsibilities
Bölüm başlığı “2.2 Layers & Responsibilities”| Layer | File | Responsibility |
|---|---|---|
| Transport | transport.ts | Singleton Nodemailer pooled SMTP connection (port 465, SSL) |
| Service | service.ts | Orchestrator: accepts {to, template, data, locale}, renders template, sends via transport |
| Templates | templates/*.tsx | React Email components — pure rendering, no side effects |
| i18n | i18n.ts | Locale-keyed string dictionaries per template |
| Tokens | tokens.ts | generateMagicToken(email) and verifyMagicToken(token) — DB-backed |
2.3 Service API
Bölüm başlığı “2.3 Service API”interface SendMailOptions { to: string; template: 'magic-link'; // Union grows as templates are added data: Record<string, unknown>; locale?: string; // Defaults to 'en'}
async function sendMail(options: SendMailOptions): Promise<{ success: boolean; messageId?: string; error?: string }>;3. Database Schema
Bölüm başlığı “3. Database Schema”3.1 New Model: MagicLinkToken
Bölüm başlığı “3.1 New Model: MagicLinkToken”model MagicLinkToken { id Int @id @default(autoincrement()) email String token String @unique // crypto.randomBytes(32).toString('hex') expiresAt DateTime // created + 15 minutes usedAt DateTime? // Set on first use (single-use enforcement) ipAddress String? userAgent String? createdAt DateTime @default(now())
@@index([email]) @@index([token])}No changes to existing models. The magic link flow creates a standard NextAuth session upon successful verification.
4. Magic Link Auth Flow
Bölüm başlığı “4. Magic Link Auth Flow”4.1 Send Flow
Bölüm başlığı “4.1 Send Flow”POST /api/auth/magic-link/sendBody: { email: string }
1. Validate email format2. Rate limit: 5 requests/hour per email, 10 requests/hour per IP3. Look up AdminUser by email4. If NOT found → return same success response (email enumeration protection)5. If found: a. Delete any existing unused tokens for this email b. Generate token: crypto.randomBytes(32).toString('hex') c. Insert MagicLinkToken: { email, token, expiresAt: now + 15min, ipAddress, userAgent } d. Render magic-link template with React Email (using admin's locale or 'en') e. Send via mail service6. Return: { success: true, message: "Check your email" }4.2 Verify Flow
Bölüm başlığı “4.2 Verify Flow”GET /api/auth/magic-link/verify?token=xxx
1. Find MagicLinkToken by token2. Validate: a. Token exists → else 400 "Invalid or expired link" b. expiresAt > now → else 400 "Link expired" c. usedAt is null → else 400 "Link already used"3. Mark token as used: UPDATE usedAt = now()4. Find AdminUser by email5. Create NextAuth session (programmatic signIn)6. If admin has totpEnabled: a. Clear twoFactorVerifiedAt (same as password login) b. Redirect to 2FA verification page7. If no 2FA → redirect to /admin/dashboard4.3 Security Measures
Bölüm başlığı “4.3 Security Measures”| Measure | Implementation |
|---|---|
| Single-use tokens | usedAt column — checked on verify, set after use |
| 15-minute TTL | expiresAt column — checked on verify |
| Rate limiting | Per-email: 5/hour, Per-IP: 10/hour (reuse existing rateLimit()) |
| Email enumeration | Same response whether user exists or not |
| 2FA preserved | Magic link skips password only — 2FA step still required if enabled |
| Token entropy | crypto.randomBytes(32) = 256 bits of randomness |
| HTTPS only | Magic link URL uses MAGIC_LINK_BASE_URL (https://ecutuningportal.com) |
| Cleanup | Delete unused tokens for same email before creating new one |
5. Email Template Design
Bölüm başlığı “5. Email Template Design”5.1 Base Layout
Bölüm başlığı “5.1 Base Layout”All emails wrapped in a consistent branded layout:
- Background:
#111111(slightly lighter than#050505for email client compatibility) - Content panel:
#1a1a1awith subtle border - Accent:
#ef4444(brand-red) for buttons, links, highlights - Font: System font stack with Chakra Petch as preferred (fallback to sans-serif)
- Header: CPU icon + “ECU TUNING PORTAL” text
- Footer: Copyright, auto-generated notice, website link
5.2 Magic Link Template
Bölüm başlığı “5.2 Magic Link Template”Subject (en): “Sign in to ECU Tuning Portal” Subject (tr): “ECU Tuning Portal Giris”
Body:
Hello {name},
Click the button below to sign in to your account.This link is valid for 15 minutes and can only be used once.
[ Sign In → ] ← Red CTA button (#ef4444)
If you did not request this link, you can safely ignore this email.
IP: {ipAddress}5.3 i18n Structure
Bölüm başlığı “5.3 i18n Structure”type TemplateTranslations = Record<string, Record<string, string>>;
const translations: Record<string, TemplateTranslations> = { 'magic-link': { en: { subject: 'Sign in to ECU Tuning Portal', greeting: 'Hello {name}', body: 'Click the button below to sign in...', button: 'Sign In', warning: 'If you did not request this link...', expiry: 'This link is valid for 15 minutes...', }, tr: { subject: 'ECU Tuning Portal Giris', greeting: 'Merhaba {name}', body: 'Hesabiniza giris yapmak icin asagidaki butona tiklayin...', button: 'Giris Yap', warning: 'Bu linki siz talep etmediyseniz...', expiry: 'Bu link 15 dakika gecerlidir...', }, },};Locale support starts with en and tr. The structure supports all 24 locales — adding a new language only requires adding a key to the translations object.
6. Admin Login Page Changes
Bölüm başlığı “6. Admin Login Page Changes”Current State
Bölüm başlığı “Current State”- Email + password form
- Redirects to 2FA page if TOTP enabled
New State (Hybrid)
Bölüm başlığı “New State (Hybrid)”- Tab/toggle approach: Two sections separated by “or” divider
- Email + password form (existing, unchanged)
- “Sign in with email link” — email-only form + send button
- After magic link sent: success message “Check your email” with countdown/resend option
- Visual style matches existing glass-panel dark theme
UI Flow
Bölüm başlığı “UI Flow”┌─────────────────────────────────┐│ ECU TUNING PORTAL ││ Admin Panel Login │├─────────────────────────────────┤│ Email: [______________] ││ Password: [______________] ││ ││ [ Sign In ] ││ ││ ─────── or ──────── ││ ││ [ Sign in with Email Link ] │ ← Expands to email-only form│ │└─────────────────────────────────┘7. Environment Variables
Bölüm başlığı “7. Environment Variables”# Mail Service (new)SMTP_HOST=ecutuningportal.comSMTP_PORT=465SMTP_SECURE=trueSMTP_USER=no-reply@ecutuningportal.comSMTP_PASS=<from Plesk>SMTP_FROM="ECU Tuning Portal <no-reply@ecutuningportal.com>"
# Magic Link (new)MAGIC_LINK_BASE_URL=https://ecutuningportal.com8. Dependencies
Bölüm başlığı “8. Dependencies”New Production Dependencies
Bölüm başlığı “New Production Dependencies”| Package | Purpose |
|---|---|
@react-email/components | React Email UI components (Html, Body, Button, etc.) |
nodemailer | SMTP email transport |
New Dev Dependencies
Bölüm başlığı “New Dev Dependencies”| Package | Purpose |
|---|---|
@types/nodemailer | TypeScript type definitions |
9. File Change Summary
Bölüm başlığı “9. File Change Summary”| File | Action | Description |
|---|---|---|
lib/mail/transport.ts | Create | Nodemailer SMTP singleton with connection pooling |
lib/mail/service.ts | Create | Main sendMail() orchestrator |
lib/mail/tokens.ts | Create | generateMagicToken / verifyMagicToken |
lib/mail/i18n.ts | Create | Template translation dictionaries |
lib/mail/templates/base-layout.tsx | Create | Shared branded email layout |
lib/mail/templates/magic-link.tsx | Create | Magic link email template |
lib/mail/templates/index.ts | Create | Template registry |
prisma/schema.prisma | Edit | Add MagicLinkToken model |
app/api/auth/magic-link/send/route.ts | Create | Send magic link API |
app/api/auth/magic-link/verify/route.ts | Create | Verify magic link API |
app/[locale]/admin/login/page.tsx | Edit | Add magic link UI option |
.env | Edit | Add SMTP + magic link env vars |
package.json | Edit | Add new dependencies |
10. Future Extensibility
Bölüm başlığı “10. Future Extensibility”This design enables future additions with minimal changes:
- New templates: Create a new
.tsxfile intemplates/, add translations toi18n.ts, add type toSendMailOptions.templateunion - Customer portal: Same
sendMail()call, just different templates and token logic referencing Customer model instead of AdminUser - Landing page: Contact form confirmations, newsletter — just new templates
- Attachments:
sendMail()options can be extended to acceptattachmentsarray (Nodemailer native support) - Email logs: Add an
EmailLogmodel to track all sent emails for debugging/audit