first commit
This commit is contained in:
144
docs/SMTP-SETUP.md
Normal file
144
docs/SMTP-SETUP.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
2. Generate encryption key:
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
3. Update SMTP settings in `.env.local`:
|
||||
```env
|
||||
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>
|
||||
```
|
||||
|
||||
### Method 2: Admin Panel (Recommended)
|
||||
|
||||
**IMPORTANT:** The `ENCRYPTION_KEY` environment variable is **required** for database-stored SMTP configuration. Generate and set it before using the admin panel:
|
||||
|
||||
```bash
|
||||
# Generate encryption key
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
|
||||
# Add to your environment variables (e.g., .env.local)
|
||||
ENCRYPTION_KEY=<generated-key>
|
||||
```
|
||||
|
||||
Steps:
|
||||
1. Ensure `ENCRYPTION_KEY` is set in your environment
|
||||
2. Restart the application server to load the new environment variable
|
||||
3. Log in as admin
|
||||
4. Navigate to **Settings** → **SMTP Settings**
|
||||
5. Fill in SMTP configuration
|
||||
6. Click **Test Connection** to verify
|
||||
7. 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
- **For Gmail:** Use App Password, not account password
|
||||
- Generate at: https://myaccount.google.com/apppasswords
|
||||
- Enable 2FA first before creating App Passwords
|
||||
- Verify username and password have no trailing spaces
|
||||
- Check if SMTP is enabled for your account
|
||||
- **Database config users:** Ensure `ENCRYPTION_KEY` is set and server was restarted
|
||||
- If using database config after upgrading, click "Reset to Defaults" and re-enter credentials
|
||||
|
||||
### 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
|
||||
555
docs/plans/2025-11-17-smtp-integration-design.md
Normal file
555
docs/plans/2025-11-17-smtp-integration-design.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# SMTP Integration Design
|
||||
|
||||
**Datum:** 2025-11-17
|
||||
**Feature:** SMTP Integration für E-Mail-Versand (Welcome Mails & Password Reset)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Integration eines flexiblen SMTP-Systems in die Location Tracker App für automatische E-Mail-Benachrichtigungen. Hybrid-Ansatz mit .env-Fallback und Admin-Panel-Konfiguration.
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Funktionale Anforderungen
|
||||
- Welcome-E-Mails beim Erstellen neuer User
|
||||
- Password-Reset-Flow für User
|
||||
- SMTP-Konfiguration über Admin-Panel
|
||||
- Live-Vorschau aller E-Mail-Templates
|
||||
- Test-E-Mail-Versand zur Validierung
|
||||
- Fallback auf .env wenn DB-Config leer
|
||||
|
||||
### Nicht-funktionale Anforderungen
|
||||
- Verschlüsselte Speicherung von SMTP-Passwörtern
|
||||
- Rate-Limiting für Test-E-Mails
|
||||
- Fehlertoleranz (User-Erstellung funktioniert auch bei E-Mail-Fehler)
|
||||
- Mobile-responsive Admin-UI
|
||||
|
||||
## Architektur
|
||||
|
||||
### 1. Datenbank-Schema
|
||||
|
||||
**Neue Tabelle: `settings`** (in `database.sqlite`)
|
||||
```sql
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**SMTP-Config als JSON in `settings.value`:**
|
||||
```json
|
||||
{
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "user@example.com",
|
||||
"pass": "encrypted_password_here"
|
||||
},
|
||||
"from": {
|
||||
"email": "noreply@example.com",
|
||||
"name": "Location Tracker"
|
||||
},
|
||||
"replyTo": "support@example.com",
|
||||
"timeout": 10000
|
||||
}
|
||||
```
|
||||
|
||||
**Neue Tabelle: `password_reset_tokens`** (in `database.sqlite`)
|
||||
```sql
|
||||
CREATE TABLE password_reset_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Umgebungsvariablen (.env)
|
||||
|
||||
Neue Variablen für Fallback-Config:
|
||||
```env
|
||||
# SMTP Configuration (Fallback)
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=user@example.com
|
||||
SMTP_PASS=password
|
||||
SMTP_FROM_EMAIL=noreply@example.com
|
||||
SMTP_FROM_NAME=Location Tracker
|
||||
|
||||
# Security
|
||||
ENCRYPTION_KEY=<32-byte-hex-string>
|
||||
```
|
||||
|
||||
### 3. API-Struktur
|
||||
|
||||
#### Admin-APIs (Authentifizierung erforderlich, role: admin)
|
||||
|
||||
**`GET /api/admin/settings/smtp`**
|
||||
- Gibt aktuelle SMTP-Config zurück (Passwort maskiert)
|
||||
- Response: `{ config: SMTPConfig | null, source: 'database' | 'env' }`
|
||||
|
||||
**`POST /api/admin/settings/smtp`**
|
||||
- Speichert SMTP-Einstellungen in Datenbank
|
||||
- Body: `SMTPConfig`
|
||||
- Validiert alle Felder
|
||||
- Verschlüsselt Passwort vor Speicherung
|
||||
|
||||
**`POST /api/admin/settings/smtp/test`**
|
||||
- Testet SMTP-Verbindung ohne zu speichern
|
||||
- Body: `{ config: SMTPConfig, testEmail: string }`
|
||||
- Sendet Test-E-Mail an angegebene Adresse
|
||||
|
||||
**`GET /api/admin/emails/preview?template=welcome`**
|
||||
- Rendert E-Mail-Template als HTML
|
||||
- Query: `template` ("welcome" | "password-reset")
|
||||
- Verwendet Beispiel-Daten
|
||||
|
||||
**`POST /api/admin/emails/send-test`**
|
||||
- Sendet Test-E-Mail mit echtem Template
|
||||
- Body: `{ template: string, email: string }`
|
||||
- Rate-Limit: 5 pro Minute
|
||||
|
||||
#### Auth-APIs (Public)
|
||||
|
||||
**`POST /api/auth/forgot-password`**
|
||||
- Body: `{ email: string }`
|
||||
- Generiert Token, sendet Reset-E-Mail
|
||||
- Response: Immer Success (Security: kein User-Enumeration)
|
||||
|
||||
**`POST /api/auth/reset-password`**
|
||||
- Body: `{ token: string, newPassword: string }`
|
||||
- Validiert Token, setzt neues Passwort
|
||||
- Markiert Token als "used"
|
||||
|
||||
### 4. E-Mail-Service
|
||||
|
||||
**Zentrale Klasse: `EmailService`** (`/lib/email-service.ts`)
|
||||
|
||||
```typescript
|
||||
class EmailService {
|
||||
async sendWelcomeEmail(user: User, temporaryPassword?: string): Promise<void>
|
||||
async sendPasswordReset(user: User, token: string): Promise<void>
|
||||
|
||||
private async getConfig(): Promise<SMTPConfig>
|
||||
private async sendEmail(to: string, subject: string, html: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
**Funktionsweise:**
|
||||
1. Lädt SMTP-Config aus DB (Key: `smtp_config`)
|
||||
2. Falls leer → Lädt aus .env
|
||||
3. Entschlüsselt Passwort
|
||||
4. Erstellt Nodemailer-Transport
|
||||
5. Rendert React Email Template → HTML
|
||||
6. Sendet E-Mail mit Fehlerbehandlung
|
||||
|
||||
### 5. React Email Templates
|
||||
|
||||
**Verzeichnisstruktur:**
|
||||
```
|
||||
/emails/
|
||||
├── components/
|
||||
│ ├── email-layout.tsx # Basis-Layout für alle E-Mails
|
||||
│ ├── email-header.tsx # Header mit Logo
|
||||
│ └── email-footer.tsx # Footer mit Links
|
||||
├── welcome.tsx # Welcome-E-Mail Template
|
||||
└── password-reset.tsx # Password-Reset Template
|
||||
```
|
||||
|
||||
**Template-Props:**
|
||||
```typescript
|
||||
// welcome.tsx
|
||||
interface WelcomeEmailProps {
|
||||
username: string;
|
||||
loginUrl: string;
|
||||
temporaryPassword?: string;
|
||||
}
|
||||
|
||||
// password-reset.tsx
|
||||
interface PasswordResetEmailProps {
|
||||
username: string;
|
||||
resetUrl: string;
|
||||
expiresIn: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Inline-Styles für maximale Kompatibilität
|
||||
- Responsive Design
|
||||
- Automatische Plain-Text-Alternative
|
||||
- Wiederverwendbare Komponenten
|
||||
|
||||
### 6. Admin-Panel UI
|
||||
|
||||
#### Neue Navigation
|
||||
|
||||
Erweitere `/admin/layout.tsx`:
|
||||
```typescript
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/admin" },
|
||||
{ name: "Devices", href: "/admin/devices" },
|
||||
{ name: "Users", href: "/admin/users" },
|
||||
{ name: "Settings", href: "/admin/settings" }, // NEU
|
||||
{ name: "Emails", href: "/admin/emails" }, // NEU
|
||||
];
|
||||
```
|
||||
|
||||
#### `/admin/settings` - SMTP-Konfiguration
|
||||
|
||||
**Layout:**
|
||||
- Tab-Navigation: "SMTP Settings" (weitere Tabs vorbereitet)
|
||||
- Formular mit Feldern:
|
||||
- Host (Text)
|
||||
- Port (Number)
|
||||
- Secure (Toggle: TLS/SSL)
|
||||
- Username (Text)
|
||||
- Password (Password, zeigt *** wenn gesetzt)
|
||||
- From Email (Email)
|
||||
- From Name (Text)
|
||||
- Reply-To (Email, optional)
|
||||
- Timeout (Number, ms)
|
||||
|
||||
**Buttons:**
|
||||
- "Test Connection" → Modal für Test-E-Mail-Adresse
|
||||
- "Save Settings" → Speichert in DB
|
||||
- "Reset to Defaults" → Lädt .env-Werte
|
||||
|
||||
**Validierung:**
|
||||
- Live-Validierung bei Eingabe
|
||||
- Port: 1-65535
|
||||
- E-Mail-Format prüfen
|
||||
- Required-Felder markieren
|
||||
|
||||
#### `/admin/emails` - E-Mail-Vorschau
|
||||
|
||||
**Layout:**
|
||||
- Links: Liste aller Templates
|
||||
- Rechts: Live-Vorschau (iframe mit gerendertem HTML)
|
||||
- Mobile: Gestapelt
|
||||
|
||||
**Features:**
|
||||
- Template auswählen → Vorschau aktualisiert
|
||||
- "Send Test Email" Button → Modal für E-Mail-Adresse
|
||||
- "Edit Template" Link (führt zur Datei, Info-Text)
|
||||
|
||||
#### `/admin/users` - Erweiterungen
|
||||
|
||||
Neue Buttons pro User:
|
||||
- "Resend Welcome Email"
|
||||
- "Send Password Reset"
|
||||
|
||||
### 7. User-facing Pages
|
||||
|
||||
#### `/forgot-password`
|
||||
|
||||
**UI:**
|
||||
- E-Mail-Eingabefeld
|
||||
- "Send Reset Link" Button
|
||||
- Link zurück zu `/login`
|
||||
|
||||
**Flow:**
|
||||
1. User gibt E-Mail ein
|
||||
2. POST zu `/api/auth/forgot-password`
|
||||
3. Success-Message (immer, auch bei ungültiger E-Mail)
|
||||
4. E-Mail mit Reset-Link wird versendet
|
||||
|
||||
#### `/reset-password?token=xxx`
|
||||
|
||||
**UI:**
|
||||
- Neues Passwort eingeben (2x)
|
||||
- "Reset Password" Button
|
||||
|
||||
**Validierung:**
|
||||
- Token gültig?
|
||||
- Token nicht abgelaufen?
|
||||
- Token nicht bereits verwendet?
|
||||
- Passwort-Stärke prüfen
|
||||
|
||||
**Flow:**
|
||||
1. Token validieren (onLoad)
|
||||
2. Bei ungültig: Fehler anzeigen
|
||||
3. User gibt neues Passwort ein
|
||||
4. POST zu `/api/auth/reset-password`
|
||||
5. Erfolg → Redirect zu `/login`
|
||||
|
||||
#### `/login` - Erweiterung
|
||||
|
||||
Neuer Link unter Login-Form:
|
||||
```tsx
|
||||
<Link href="/forgot-password">Forgot Password?</Link>
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
### Verschlüsselung
|
||||
|
||||
**SMTP-Passwort:**
|
||||
- Algorithmus: AES-256-GCM
|
||||
- Key: `process.env.ENCRYPTION_KEY` (32-Byte-Hex)
|
||||
- Verschlüsselt vor DB-Speicherung
|
||||
- Entschlüsselt beim Abrufen
|
||||
|
||||
**Implementierung:** `/lib/crypto-utils.ts`
|
||||
```typescript
|
||||
function encrypt(text: string): string
|
||||
function decrypt(encryptedText: string): string
|
||||
```
|
||||
|
||||
### Authentifizierung & Autorisierung
|
||||
|
||||
- Alle `/api/admin/*` prüfen `next-auth` Session
|
||||
- User muss `role: "admin"` haben
|
||||
- Unauthorized → 401 Response
|
||||
|
||||
### Rate-Limiting
|
||||
|
||||
**Test-E-Mail-Versand:**
|
||||
- Max. 5 Test-E-Mails pro Minute
|
||||
- Pro IP-Adresse
|
||||
- 429 Response bei Überschreitung
|
||||
|
||||
### Input-Validierung
|
||||
|
||||
- E-Mail-Adressen: RFC 5322 Format
|
||||
- Port: 1-65535
|
||||
- Timeout: > 0
|
||||
- SQL-Injection-Schutz durch Prepared Statements
|
||||
|
||||
### Token-Sicherheit
|
||||
|
||||
**Password-Reset-Tokens:**
|
||||
- UUID v4 (kryptografisch sicher)
|
||||
- Gültigkeitsdauer: 1 Stunde
|
||||
- One-Time-Use (used-Flag)
|
||||
- Automatisches Cleanup alter Tokens (Cron-Job)
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### E-Mail-Versand-Fehler
|
||||
|
||||
**Strategie: Fail-Soft**
|
||||
- User-Erstellung schlägt NICHT fehl bei E-Mail-Fehler
|
||||
- Fehler wird geloggt
|
||||
- Admin-Benachrichtigung im Dashboard (später)
|
||||
- "Resend" Option verfügbar
|
||||
|
||||
**Error-Logging:**
|
||||
```typescript
|
||||
console.error('[EmailService] Failed to send email:', {
|
||||
type: 'welcome' | 'password-reset',
|
||||
recipient: user.email,
|
||||
error: error.message
|
||||
});
|
||||
```
|
||||
|
||||
### SMTP-Verbindungsfehler
|
||||
|
||||
**Im Admin-Panel:**
|
||||
- Detaillierte Fehlermeldung anzeigen
|
||||
- Vorschläge zur Fehlerbehebung
|
||||
- Link zur SMTP-Provider-Dokumentation
|
||||
|
||||
**Typische Fehler:**
|
||||
- Authentication failed → Credentials prüfen
|
||||
- Connection timeout → Firewall/Port prüfen
|
||||
- TLS error → secure-Flag anpassen
|
||||
|
||||
## Testing
|
||||
|
||||
### Development
|
||||
|
||||
**Mailtrap/Ethereal für SMTP-Tests:**
|
||||
```env
|
||||
SMTP_HOST=smtp.ethereal.email
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=<ethereal-user>
|
||||
SMTP_PASS=<ethereal-pass>
|
||||
```
|
||||
|
||||
**React Email Dev-Server:**
|
||||
```bash
|
||||
npm run email:dev
|
||||
```
|
||||
Öffnet Browser mit Vorschau aller Templates
|
||||
|
||||
### Admin-Panel Testing
|
||||
|
||||
1. SMTP-Config eingeben
|
||||
2. "Test Connection" klicken
|
||||
3. Test-E-Mail-Adresse eingeben
|
||||
4. E-Mail empfangen & Inhalt prüfen
|
||||
5. "Save Settings" klicken
|
||||
6. Page-Reload → Config bleibt erhalten
|
||||
|
||||
### Integration Testing
|
||||
|
||||
**Welcome-E-Mail:**
|
||||
1. Neuen User in `/admin/users` erstellen
|
||||
2. Welcome-E-Mail wird versendet
|
||||
3. E-Mail empfangen & prüfen
|
||||
|
||||
**Password-Reset:**
|
||||
1. `/forgot-password` öffnen
|
||||
2. E-Mail eingeben & absenden
|
||||
3. Reset-E-Mail empfangen
|
||||
4. Link klicken → `/reset-password` öffnet
|
||||
5. Neues Passwort setzen
|
||||
6. Mit neuem Passwort einloggen
|
||||
|
||||
## Deployment
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `npm install` für neue Dependencies
|
||||
- [ ] `ENCRYPTION_KEY` in `.env` generieren
|
||||
- [ ] SMTP-Credentials in `.env` setzen (optional, Fallback)
|
||||
- [ ] `npm run db:init:app` (neue Tabellen erstellen)
|
||||
- [ ] Server-Neustart
|
||||
- [ ] SMTP im Admin-Panel konfigurieren
|
||||
- [ ] Test-E-Mail senden & empfangen
|
||||
- [ ] Password-Reset-Flow einmal komplett testen
|
||||
|
||||
### Migration-Script
|
||||
|
||||
Erweitere `/scripts/init-database.js`:
|
||||
```javascript
|
||||
// Neue Tabellen erstellen
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Index für Performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_reset_tokens_user_id
|
||||
ON password_reset_tokens(user_id);
|
||||
`);
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### NPM-Packages
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.9.8",
|
||||
"react-email": "^2.1.0",
|
||||
"@react-email/components": "^0.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^6.4.14"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scripts (package.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"email:dev": "email dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
/location-tracker-app/
|
||||
├── docs/
|
||||
│ └── plans/
|
||||
│ └── 2025-11-17-smtp-integration-design.md
|
||||
├── emails/
|
||||
│ ├── components/
|
||||
│ │ ├── email-layout.tsx
|
||||
│ │ ├── email-header.tsx
|
||||
│ │ └── email-footer.tsx
|
||||
│ ├── welcome.tsx
|
||||
│ └── password-reset.tsx
|
||||
├── lib/
|
||||
│ ├── email-service.ts
|
||||
│ ├── crypto-utils.ts
|
||||
│ └── email-renderer.ts
|
||||
├── app/
|
||||
│ ├── admin/
|
||||
│ │ ├── settings/
|
||||
│ │ │ └── page.tsx
|
||||
│ │ ├── emails/
|
||||
│ │ │ └── page.tsx
|
||||
│ │ └── users/
|
||||
│ │ └── page.tsx (erweitern)
|
||||
│ ├── forgot-password/
|
||||
│ │ └── page.tsx
|
||||
│ ├── reset-password/
|
||||
│ │ └── page.tsx
|
||||
│ └── api/
|
||||
│ ├── admin/
|
||||
│ │ ├── settings/
|
||||
│ │ │ └── smtp/
|
||||
│ │ │ ├── route.ts
|
||||
│ │ │ └── test/route.ts
|
||||
│ │ └── emails/
|
||||
│ │ ├── preview/route.ts
|
||||
│ │ └── send-test/route.ts
|
||||
│ └── auth/
|
||||
│ ├── forgot-password/route.ts
|
||||
│ └── reset-password/route.ts
|
||||
└── scripts/
|
||||
└── init-database.js (erweitern)
|
||||
```
|
||||
|
||||
## Zukünftige Erweiterungen
|
||||
|
||||
### Phase 2 (Optional)
|
||||
- Weitere E-Mail-Templates (Geofence-Alerts, Reports)
|
||||
- E-Mail-Queue für Bulk-Versand
|
||||
- E-Mail-Versand-Statistiken im Dashboard
|
||||
- Attachments-Support
|
||||
- Multiple SMTP-Provider (Failover)
|
||||
- E-Mail-Template-Editor im Admin-Panel
|
||||
|
||||
### Phase 3 (Optional)
|
||||
- Alternative zu SMTP: SendGrid/Mailgun/Resend API
|
||||
- Webhook für E-Mail-Events (Delivered, Opened, Clicked)
|
||||
- Unsubscribe-Verwaltung
|
||||
- E-Mail-Preferences pro User
|
||||
|
||||
## Offene Fragen
|
||||
|
||||
- ✅ SMTP-Parameter: Erweiterte Konfiguration gewählt
|
||||
- ✅ E-Mail-Bibliothek: Nodemailer gewählt
|
||||
- ✅ Template-Engine: React Email gewählt
|
||||
- ✅ Vorschau-Strategie: Live-Vorschau im Admin-Panel
|
||||
- ✅ Config-Speicherung: Hybrid-Ansatz (DB + .env Fallback)
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- [ ] Admin kann SMTP-Settings im Panel konfigurieren
|
||||
- [ ] Test-E-Mail-Versand funktioniert
|
||||
- [ ] Welcome-E-Mail wird bei User-Erstellung gesendet
|
||||
- [ ] Password-Reset-Flow funktioniert komplett
|
||||
- [ ] E-Mail-Vorschau zeigt alle Templates korrekt an
|
||||
- [ ] SMTP-Passwort wird verschlüsselt gespeichert
|
||||
- [ ] Fallback auf .env funktioniert
|
||||
- [ ] Fehlerbehandlung zeigt sinnvolle Meldungen
|
||||
- [ ] Mobile-responsive Admin-UI
|
||||
- [ ] Alle Tests erfolgreich
|
||||
3453
docs/plans/2025-11-17-smtp-integration.md
Normal file
3453
docs/plans/2025-11-17-smtp-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
208
docs/plans/2025-11-23-device-access-control-security-fix.md
Normal file
208
docs/plans/2025-11-23-device-access-control-security-fix.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Device Access Control Security Fix
|
||||
|
||||
**Date:** 2025-11-23
|
||||
**Issue:** Users could see all devices in the system, regardless of ownership
|
||||
**Severity:** Medium (Information Disclosure)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The `/admin` dashboard and `/map` page were calling `/api/devices/public`, which returned **all devices** without authentication or filtering. This meant:
|
||||
- Any authenticated user could see device names, IDs, and colors for all devices
|
||||
- Regular ADMIN users could see devices owned by other users
|
||||
- VIEWER users could see devices they shouldn't have access to
|
||||
|
||||
## Root Cause
|
||||
|
||||
The `/api/devices/public` endpoint:
|
||||
1. Had no authentication check
|
||||
2. Called `deviceDb.findAll()` without any `userId` filter
|
||||
3. Was designed as a "public" endpoint despite containing sensitive information
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
### 1. Created Centralized Access Control Helper (`lib/db.ts`)
|
||||
|
||||
Added `userDb.getAllowedDeviceIds(userId, role, username)` function that implements role-based access control:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Get list of device IDs that a user is allowed to access
|
||||
* @param userId - The user's ID
|
||||
* @param role - The user's role (ADMIN, VIEWER)
|
||||
* @param username - The user's username (for super admin check)
|
||||
* @returns Array of device IDs the user can access
|
||||
*/
|
||||
getAllowedDeviceIds: (userId: string, role: string, username: string): string[]
|
||||
```
|
||||
|
||||
**Access Control Rules:**
|
||||
- **Super Admin** (`username === "admin"`): Sees ALL devices
|
||||
- **VIEWER** (with `parent_user_id`): Sees parent user's devices only
|
||||
- **Regular ADMIN**: Sees only their own devices (`ownerId = userId`)
|
||||
- **Others**: No access (empty array)
|
||||
|
||||
### 2. Secured `/api/devices/public` Endpoint
|
||||
|
||||
**Changes to `app/api/devices/public/route.ts`:**
|
||||
- Added `auth()` check at the beginning
|
||||
- Returns 401 if no session
|
||||
- Uses `userDb.getAllowedDeviceIds()` to filter devices
|
||||
- Maintains backward-compatible response format
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
export async function GET() {
|
||||
const devices = deviceDb.findAll();
|
||||
// Returns ALL devices without auth
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const allowedDeviceIds = userDb.getAllowedDeviceIds(userId, role, username);
|
||||
const userDevices = allDevices.filter(device =>
|
||||
allowedDeviceIds.includes(device.id)
|
||||
);
|
||||
// Returns only devices user can access
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Enhanced `/api/locations` Endpoint
|
||||
|
||||
**Changes to `app/api/locations/route.ts`:**
|
||||
- Replaced manual parent user lookup with centralized `getAllowedDeviceIds()`
|
||||
- Ensures location data is filtered to only show locations from owned devices
|
||||
- Simplified code by removing duplicate logic
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
let targetUserId = userId;
|
||||
if (currentUser?.role === 'VIEWER' && currentUser.parent_user_id) {
|
||||
targetUserId = currentUser.parent_user_id;
|
||||
}
|
||||
const userDevices = deviceDb.findAll({ userId: targetUserId });
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
const userDeviceIds = userDb.getAllowedDeviceIds(userId, role, sessionUsername);
|
||||
// Filter locations to only include user's devices
|
||||
locations = locations.filter(loc => userDeviceIds.includes(loc.username));
|
||||
```
|
||||
|
||||
## Testing Results
|
||||
|
||||
Created test script `scripts/test-device-access.js` to verify access control.
|
||||
|
||||
**Test Results:**
|
||||
```
|
||||
User: admin (ADMIN)
|
||||
Can see devices: 10, 11, 12, 15 ✓ (ALL devices)
|
||||
|
||||
User: joachim (ADMIN)
|
||||
Can see devices: 12, 15 ✓ (only owned devices)
|
||||
|
||||
User: hummel (VIEWER, parent: joachim)
|
||||
Can see devices: 12, 15 ✓ (parent's devices)
|
||||
|
||||
User: joachiminfo (ADMIN, no devices)
|
||||
Can see devices: NONE ✓ (no owned devices)
|
||||
```
|
||||
|
||||
All tests passed! Each user sees only the devices they should have access to.
|
||||
|
||||
## Impact
|
||||
|
||||
### Security Improvements
|
||||
- ✅ Users can no longer see devices they don't own
|
||||
- ✅ Device ownership is enforced at the API level
|
||||
- ✅ Location data is filtered by device ownership
|
||||
- ✅ Centralized access control logic prevents future bugs
|
||||
|
||||
### User Experience
|
||||
- No breaking changes for legitimate users
|
||||
- Each user sees only their own data
|
||||
- Super admin retains full visibility for system administration
|
||||
- VIEWER users see their parent's devices as intended
|
||||
|
||||
### Files Modified
|
||||
1. `lib/db.ts` - Added `getAllowedDeviceIds()` helper function
|
||||
2. `app/api/devices/public/route.ts` - Added authentication and filtering
|
||||
3. `app/api/locations/route.ts` - Updated to use centralized access control
|
||||
4. `scripts/test-device-access.js` - New test script (can be deleted after verification)
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
### Server Restart Required
|
||||
After deploying these changes, the Next.js server must be restarted to pick up the new code:
|
||||
```bash
|
||||
pkill -f "next dev"
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Or in production:
|
||||
```bash
|
||||
npm run build
|
||||
pm2 restart location-tracker-app
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
No database migrations required. The fix uses existing columns:
|
||||
- `Device.ownerId` (already exists)
|
||||
- `User.parent_user_id` (already exists)
|
||||
- `User.role` (already exists)
|
||||
|
||||
### Backward Compatibility
|
||||
The API response format remains unchanged. Frontend components (dashboard, map) require no modifications.
|
||||
|
||||
## Future Recommendations
|
||||
|
||||
### 1. Rename Endpoint
|
||||
Consider renaming `/api/devices/public` to `/api/devices/my-devices` to better reflect its authenticated, filtered nature.
|
||||
|
||||
### 2. Add API Tests
|
||||
Create automated tests for device access control:
|
||||
- Test super admin access
|
||||
- Test regular admin access
|
||||
- Test VIEWER access with parent
|
||||
- Test VIEWER access without parent
|
||||
|
||||
### 3. Audit Other Endpoints
|
||||
Review other API endpoints for similar access control issues:
|
||||
- `/api/users` - Should users see other users?
|
||||
- `/api/mqtt/credentials` - Should credentials be filtered by user?
|
||||
|
||||
### 4. Add Logging
|
||||
Consider adding audit logging for device access:
|
||||
```typescript
|
||||
console.log(`[Access Control] User ${username} (${role}) accessed devices: ${allowedDeviceIds.join(', ')}`);
|
||||
```
|
||||
|
||||
## Verification Steps for Deployment
|
||||
|
||||
1. **Login as regular ADMIN user** (e.g., "joachim")
|
||||
- Navigate to `/admin`
|
||||
- Verify "Configured Devices" shows only devices you own
|
||||
- Verify `/map` shows only your devices
|
||||
|
||||
2. **Login as VIEWER user** (e.g., "hummel")
|
||||
- Navigate to `/admin`
|
||||
- Verify you see parent user's devices
|
||||
- Verify `/map` shows parent's devices
|
||||
|
||||
3. **Login as super admin** (username: "admin")
|
||||
- Navigate to `/admin`
|
||||
- Verify you see ALL devices
|
||||
- Verify `/map` shows all devices
|
||||
|
||||
## Conclusion
|
||||
|
||||
The device access control security issue has been successfully fixed. Users now only see devices they own (or their parent's devices for VIEWER users), with the exception of the super admin who retains full system visibility.
|
||||
|
||||
The fix is backward-compatible, requires no database changes, and centralizes access control logic for easier maintenance.
|
||||
Reference in New Issue
Block a user