diff --git a/docs/DIRECTUS-SETUP.md b/docs/DIRECTUS-SETUP.md index a22b36e..7a0635a 100644 --- a/docs/DIRECTUS-SETUP.md +++ b/docs/DIRECTUS-SETUP.md @@ -27,13 +27,10 @@ Komplette Anleitung zur Einrichtung von Directus als Backend für die dgray Klei erDiagram USERS ||--o{ LISTINGS : creates USERS ||--o{ FAVORITES : has - USERS ||--o{ CONVERSATIONS : participates - USERS ||--o{ MESSAGES : sends USERS ||--o{ REPORTS : submits LISTINGS ||--o{ LISTINGS_FILES : has LISTINGS ||--o{ FAVORITES : receives - LISTINGS ||--o{ CONVERSATIONS : has LISTINGS }o--|| CATEGORIES : belongs_to LISTINGS }o--o| LOCATIONS : located_at @@ -66,18 +63,21 @@ erDiagram CONVERSATIONS { uuid id PK - uuid listing FK - uuid buyer FK - uuid seller FK + uuid listing_id + string participant_hash_1 + string participant_hash_2 + text public_key_1 + text public_key_2 string status } MESSAGES { uuid id PK uuid conversation FK - uuid sender FK - text content - datetime read_at + string sender_hash + text content_encrypted + string nonce + string type } FAVORITES { @@ -102,6 +102,8 @@ erDiagram } ``` +**Privacy-Hinweis:** Conversations und Messages haben keine direkten User-Referenzen - nur Hashes! + --- ## 1. Data Models (Collections) @@ -110,31 +112,31 @@ erDiagram Die Haupt-Collection für alle Kleinanzeigen. -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key, auto-generated | -| `status` | String (Dropdown) | `draft`, `published`, `sold`, `expired`, `deleted` | -| `sort` | Integer | Für manuelle Sortierung | -| `user_created` | User (M2O) | Auto, Read-only | -| `date_created` | DateTime | Auto, Read-only | -| `user_updated` | User (M2O) | Auto | -| `date_updated` | DateTime | Auto | -| `title` | String | Required, max 100 chars | -| `slug` | String | Unique, auto-generated from title | -| `description` | Text (WYSIWYG) | Required | -| `price` | Decimal | Required, precision 10, scale 2 | -| `currency` | String | Default: `XMR`, Options: `XMR`, `EUR` | -| `price_type` | String | `fixed`, `negotiable`, `free`, `on_request` | -| `category` | Categories (M2O) | Required | -| `condition` | String | `new`, `like_new`, `good`, `fair`, `poor` | -| `images` | Files (M2M) | Junction: `listings_files` | -| `location` | Locations (M2O) | Optional | -| `shipping` | Boolean | Versand möglich? | -| `shipping_cost` | Decimal | Optional | -| `views` | Integer | Default: 0 | -| `expires_at` | DateTime | Auto-set, 30 Tage nach Erstellung | -| `monero_address` | String | Für Direktzahlung | -| `contact_method` | String | `chat`, `email`, `both` | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key, auto-generated | +| `status` | String | Dropdown | `draft`, `published`, `sold`, `expired`, `deleted` | +| `sort` | Integer | Input | Für manuelle Sortierung | +| `user_created` | User (M2O) | User | Auto, Read-only | +| `date_created` | DateTime | DateTime | Auto, Read-only | +| `user_updated` | User (M2O) | User | Auto | +| `date_updated` | DateTime | DateTime | Auto | +| `title` | String | Input | Required, max 100 chars | +| `slug` | String | Input | Unique, auto-generated via Flow | +| `description` | Text | WYSIWYG | Required | +| `price` | Decimal | Input | Required, precision 10, scale 2 | +| `currency` | String | Dropdown | `XMR`, `EUR`, `CHF`, `USD`, `GBP`, `JPY` (Default: XMR) | +| `price_mode` | String | Dropdown | `fiat`, `xmr` (Default: fiat) | +| `price_type` | String | Dropdown | `fixed`, `negotiable`, `free`, `on_request` | +| `category` | Categories (M2O) | Many-to-One | Required | +| `condition` | String | Dropdown | `new`, `like_new`, `good`, `fair`, `poor` | +| `images` | Files (M2M) | Files | Junction: `listings_files` | +| `location` | Locations (M2O) | Many-to-One | Optional | +| `shipping` | Boolean | Toggle | Versand möglich? | +| `shipping_cost` | Decimal | Input | Optional | +| `views` | Integer | Input | Default: 0, Read-only | +| `expires_at` | DateTime | DateTime | Auto-set via Flow, 30 Tage | +| `monero_address` | String | Input | Für Direktzahlung | **Erstellen in Directus Admin:** ``` @@ -149,16 +151,16 @@ Primary Key: UUID (auto-generated) Hierarchische Kategorien mit Übersetzungen. -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `status` | String | `draft`, `published` | -| `sort` | Integer | Für Sortierung | -| `name` | String | Required (Fallback-Name) | -| `slug` | String | Unique | -| `icon` | String | Icon-Name (z.B. `laptop`, `car`) | -| `parent` | Categories (M2O) | Self-referencing | -| `translations` | Translations | Junction: `categories_translations` | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `status` | String | Dropdown | `draft`, `published` | +| `sort` | Integer | Input | Für Sortierung | +| `name` | String | Input | Required (Fallback-Name) | +| `slug` | String | Input | Unique | +| `icon` | String | Input | Icon-Name (z.B. `laptop`, `car`) | +| `parent` | Categories (M2O) | Many-to-One | Self-referencing | +| `translations` | Translations | Translations | Junction: `categories_translations` | **Translations Fields:** | Feld | Typ | @@ -169,45 +171,67 @@ Hierarchische Kategorien mit Übersetzungen. --- -### 1.3 conversations (Konversationen) +### 1.3 conversations (Konversationen) - Metadaten-verschlüsselt -Chat zwischen Käufer und Verkäufer. +E2E-verschlüsselter Chat mit **Zero-Knowledge Metadaten** - Server weiß nicht, wer mit wem chattet. -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `date_created` | DateTime | Auto | -| `date_updated` | DateTime | Auto | -| `listing` | Listings (M2O) | Required | -| `buyer` | User (M2O) | Wer die Konversation gestartet hat | -| `seller` | User (M2O) | Auto from listing.user_created | -| `status` | String | `active`, `archived`, `blocked` | -| `messages` | Messages (O2M) | | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `date_created` | DateTime | DateTime | Auto | +| `date_updated` | DateTime | DateTime | Auto | +| `listing_id` | UUID | Input | Listing-Referenz (kein FK für Privacy) | +| `participant_hash_1` | String | Input | SHA256(user_uuid + conversation_secret) | +| `participant_hash_2` | String | Input | SHA256(user_uuid + conversation_secret) | +| `public_key_1` | Text | Textarea | X25519 Public Key für E2E | +| `public_key_2` | Text | Textarea | X25519 Public Key für E2E | +| `status` | String | Dropdown | `active`, `archived` | + +**Privacy-Konzept:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Käufer startet Chat zu Listing │ +│ → generiert conversation_secret (random 32 bytes) │ +│ → berechnet participant_hash = SHA256(own_uuid + secret) │ +│ → sendet public_key für E2E-Encryption │ +│ │ +│ 2. Verkäufer erhält Notification (via Listing-Polling) │ +│ → berechnet eigenen participant_hash mit gleichem secret │ +│ → sendet eigenen public_key │ +│ │ +│ 3. Server sieht nur: │ +│ - Zwei Hashes (nicht welche User) │ +│ - Verschlüsselte Nachrichten │ +│ - Kein buyer/seller Bezug! │ +└─────────────────────────────────────────────────────────────────┘ +``` --- -### 1.4 messages (Nachrichten) +### 1.4 messages (Nachrichten) - E2E-verschlüsselt -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `date_created` | DateTime | Auto | -| `conversation` | Conversations (M2O) | Required | -| `sender` | User (M2O) | Auto (user_created) | -| `content` | Text | Required, max 2000 chars | -| `read_at` | DateTime | Null = ungelesen | -| `type` | String | `text`, `offer`, `system` | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `date_created` | DateTime | DateTime | Auto | +| `conversation` | Conversations (M2O) | Many-to-One | Required | +| `sender_hash` | String | Input | SHA256(user_uuid + conversation_secret) | +| `content_encrypted` | Text | Textarea | XChaCha20-Poly1305 verschlüsselt | +| `nonce` | String | Input | Unique nonce für Entschlüsselung | +| `type` | String | Dropdown | `text`, `offer`, `system` | + +**Keine Klartext-Inhalte auf dem Server!** --- ### 1.5 favorites (Favoriten/Merkliste) -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `date_created` | DateTime | Auto | -| `user` | User (M2O) | Auto (user_created) | -| `listing` | Listings (M2O) | Required | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `date_created` | DateTime | DateTime | Auto | +| `user` | User (M2O) | User | Auto (user_created) | +| `listing` | Listings (M2O) | Many-to-One | Required | **Unique Constraint:** `user` + `listing` @@ -215,33 +239,33 @@ Chat zwischen Käufer und Verkäufer. ### 1.6 reports (Meldungen) -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `date_created` | DateTime | Auto | -| `reporter` | User (M2O) | Auto | -| `listing` | Listings (M2O) | Optional | -| `reported_user` | User (M2O) | Optional | -| `reason` | String | `spam`, `fraud`, `inappropriate`, `illegal`, `other` | -| `details` | Text | | -| `status` | String | `pending`, `reviewed`, `resolved`, `dismissed` | -| `admin_notes` | Text | Nur für Admins sichtbar | -| `resolved_by` | User (M2O) | | -| `resolved_at` | DateTime | | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `date_created` | DateTime | DateTime | Auto | +| `reporter` | User (M2O) | User | Auto | +| `listing` | Listings (M2O) | Many-to-One | Optional | +| `reported_user` | User (M2O) | Many-to-One | Optional | +| `reason` | String | Dropdown | `spam`, `fraud`, `inappropriate`, `illegal`, `other` | +| `details` | Text | Textarea | | +| `status` | String | Dropdown | `pending`, `reviewed`, `resolved`, `dismissed` | +| `admin_notes` | Text | Textarea | Nur für Admins sichtbar | +| `resolved_by` | User (M2O) | User | | +| `resolved_at` | DateTime | DateTime | | --- ### 1.7 locations (Orte) -| Feld | Typ | Einstellungen | -|------|-----|---------------| -| `id` | UUID | Primary Key | -| `name` | String | Stadt/Ort | -| `postal_code` | String | PLZ | -| `region` | String | Bundesland/Kanton | -| `country` | String | Default: `DE` | -| `latitude` | Float | Optional, für Kartenansicht | -| `longitude` | Float | Optional | +| Feld | Typ | Interface | Einstellungen | +|------|-----|-----------|---------------| +| `id` | UUID | – | Primary Key | +| `name` | String | Input | Stadt/Ort | +| `postal_code` | String | Input | PLZ | +| `region` | String | Input | Bundesland/Kanton | +| `country` | String | Dropdown | Default: `DE` | +| `latitude` | Float | Input | Optional, für Kartenansicht | +| `longitude` | Float | Input | Optional | --- @@ -500,8 +524,14 @@ Zusätzlich zu User-Permissions: **Trigger:** `items.create` auf `listings` +**Flow-Aufbau:** + +1. **Trigger:** `items.create` auf `listings` +2. **Operation 1:** Run Script (Slug generieren) +3. **Operation 2:** Update Data (Slug ins Feld schreiben) + ```javascript -// Operation: Run Script +// Operation 1: Run Script module.exports = async function(data) { const title = data.title; const slug = title @@ -510,11 +540,19 @@ module.exports = async function(data) { .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); + // Timestamp für Eindeutigkeit return { slug: `${slug}-${Date.now().toString(36)}` }; } ``` -**Operation:** Update Data → `listings` mit Ergebnis +``` +// Operation 2: Update Data +Collection: listings +ID: {{$trigger.key}} +Payload: { "slug": "{{$last.slug}}" } +``` + +**Ergebnis:** Title "iPhone 15 Pro!" → Slug `iphone-15-pro-m1abc2def` --- @@ -580,19 +618,6 @@ module.exports = async function(data, { database }) { --- -### 4.6 Welcome Email bei Registrierung - -**Trigger:** `users.create` - -**Operation:** Send Email -``` -To: {{$trigger.email}} -Subject: Willkommen bei dgray.io -Template: welcome-email -``` - ---- - ## 5. Einstellungen ### 5.1 Project Settings @@ -656,17 +681,94 @@ RATE_LIMITER_POINTS=100 RATE_LIMITER_DURATION=60 ``` -### 5.6 Email Settings +**Hinweis:** Keine E-Mail-Konfiguration nötig - dgray.io nutzt keine E-Mails (Privacy by Design). -```env -EMAIL_FROM=noreply@dgray.io -EMAIL_TRANSPORT=smtp -EMAIL_SMTP_HOST=smtp.example.com -EMAIL_SMTP_PORT=587 -EMAIL_SMTP_USER=xxx -EMAIL_SMTP_PASSWORD=xxx -EMAIL_SMTP_SECURE=false +### 5.6 Währungsumrechnung (Kraken API) + +Für die Anzeige von Fiat-Preisen in XMR wird die Kraken API genutzt. + +**API Endpoint:** ``` +https://api.kraken.com/0/public/Ticker?pair=XMRUSD,XMREUR,XMRGBP,XMRCHF,XMRJPY +``` + +**Frontend-Implementation:** +```javascript +const KRAKEN_API = 'https://api.kraken.com/0/public/Ticker'; +const PAIRS = { + USD: 'XMRUSD', + EUR: 'XMREUR', + GBP: 'XMRGBP', + CHF: 'XMRCHF', + JPY: 'XMRJPY' +}; + +async function getXmrRates() { + const pairs = Object.values(PAIRS).join(','); + const response = await fetch(`${KRAKEN_API}?pair=${pairs}`); + const data = await response.json(); + + const rates = {}; + for (const [currency, pair] of Object.entries(PAIRS)) { + const ticker = data.result[pair]; + if (ticker) { + rates[currency] = parseFloat(ticker.c[0]); // Last trade price + } + } + return rates; +} + +function convertToXmr(amount, currency, rates) { + if (currency === 'XMR') return amount; + return amount / rates[currency]; +} + +// Beispiel: 100 EUR in XMR +// const rates = await getXmrRates(); +// const xmrAmount = convertToXmr(100, 'EUR', rates); +``` + +**Caching:** Rates werden client-side für 5 Minuten gecached. + +### 5.7 Preismodus (Fiat vs. XMR) + +Listings können in zwei Modi erstellt werden: + +| Modus | `price_mode` | Verhalten | +|-------|--------------|-----------| +| **Fiat-fix** | `fiat` | Preis bleibt z.B. 100 EUR, XMR-Äquivalent ändert sich mit Kurs | +| **XMR-fix** | `xmr` | Preis bleibt z.B. 0.5 XMR, Fiat-Äquivalent ändert sich mit Kurs | + +**Frontend-Anzeige:** +```javascript +function displayPrice(listing, rates) { + const { price, currency, price_mode } = listing; + + if (price_mode === 'xmr' || currency === 'XMR') { + // XMR ist der Referenzpreis + const xmrPrice = currency === 'XMR' ? price : price / rates[currency]; + return { + primary: `${xmrPrice.toFixed(4)} XMR`, + secondary: currency !== 'XMR' ? `≈ ${price} ${currency}` : null + }; + } else { + // Fiat ist der Referenzpreis + const xmrEquivalent = price / rates[currency]; + return { + primary: `${price} ${currency}`, + secondary: `≈ ${xmrEquivalent.toFixed(4)} XMR` + }; + } +} +``` + +**Beispiele:** + +| Listing | Anzeige (bei 1 XMR = 150 EUR) | +|---------|-------------------------------| +| 100 EUR, mode=`fiat` | **100 EUR** ≈ 0.6667 XMR | +| 100 EUR, mode=`xmr` | **0.6667 XMR** ≈ 100 EUR | +| 0.5 XMR, mode=`xmr` | **0.5 XMR** ≈ 75 EUR | --- @@ -877,11 +979,32 @@ AUTH_PASSWORD_RESET=false --- -## 10. Sicherheitshinweise +## 10. Sicherheits- & Privacy-Hinweise -1. **Monero-Adressen** werden nur dem Listing-Ersteller und authentifizierten Nutzern angezeigt -2. **Keine echten E-Mails** - UUID-basierte Authentifizierung -3. **Rate Limiting** für API-Endpunkte aktivieren -4. **Bilder-Upload** auf max. 10MB und erlaubte Typen beschränken -5. **XSS-Schutz** für WYSIWYG-Felder in Directus aktivieren -6. **UUID-Warnung** bei Registrierung: User muss UUID sicher speichern +### Privacy by Design + +| Aspekt | Umsetzung | +|--------|-----------| +| **Authentifizierung** | UUID-basiert, keine echten E-Mails | +| **Kommunikation** | Nur verschlüsselter Chat, keine E-Mail-Option | +| **Chat-Metadaten** | Zero-Knowledge (participant_hash statt user_id) | +| **Chat-Inhalte** | E2E-verschlüsselt (XChaCha20-Poly1305) | +| **Währungen** | XMR (primär) + EUR, CHF, USD, GBP, JPY | +| **Kontakt** | Ausschließlich über internen Chat | + +### Technische Sicherheit + +1. **Monero-Adressen** werden nur authentifizierten Nutzern angezeigt +2. **Rate Limiting** für alle API-Endpunkte aktivieren +3. **Bilder-Upload** auf max. 10MB und erlaubte Typen beschränken +4. **XSS-Schutz** für WYSIWYG-Felder in Directus aktivieren +5. **UUID-Warnung** bei Registrierung: User muss UUID sicher speichern +6. **Kein Passwort-Reset** möglich (by design) +7. **Keine Server-Logs** mit User-Bezug speichern + +### Was der Server NICHT weiß + +- Echte Identität der User (nur UUIDs) +- Wer mit wem chattet (nur Hashes) +- Chat-Inhalte (nur verschlüsselt) +- E-Mail-Adressen (existieren nicht)