# Directus Setup für kashilo.com Komplette Anleitung zur Einrichtung von Directus als Backend für die kashilo Kleinanzeigen-PWA. **API URL**: https://api.kashilo.com/ --- ## Inhaltsverzeichnis 1. [Data Models (Collections)](#1-data-models-collections) 2. [User Roles](#2-user-roles) 3. [Access Policies](#3-access-policies) 4. [Flows (Automatisierungen)](#4-flows-automatisierungen) 5. [Einstellungen](#5-einstellungen) 6. [Frontend-Integration](#6-frontend-integration) 7. [Initiale Daten](#7-initiale-daten) 8. [Checkliste](#8-checkliste) 9. [Anonyme Authentifizierung (UUID-basiert)](#9-anonyme-authentifizierung-uuid-basiert) 10. [Sicherheitshinweise](#10-sicherheitshinweise) --- ## Datenmodell-Übersicht ```mermaid erDiagram USERS ||--o{ LISTINGS : creates USERS ||--o{ FAVORITES : has USERS ||--o{ REPORTS : submits LISTINGS ||--o{ LISTINGS_FILES : has LISTINGS ||--o{ FAVORITES : receives LISTINGS }o--|| CATEGORIES : belongs_to LISTINGS }o--o| LOCATIONS : located_at LISTINGS_FILES }o--|| DIRECTUS_FILES : references CATEGORIES ||--o{ CATEGORIES : has_children CATEGORIES ||--o{ CATEGORIES_TRANSLATIONS : has CONVERSATIONS ||--o{ MESSAGES : contains LISTINGS { uuid id PK string status string title text description decimal price string currency string condition datetime expires_at string monero_address } CATEGORIES { uuid id PK string name string slug string icon uuid parent FK } CONVERSATIONS { uuid id PK 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 string sender_hash text content_encrypted string nonce string type } FAVORITES { uuid id PK uuid user FK uuid listing FK } REPORTS { uuid id PK uuid reporter FK uuid listing FK string reason string status } LOCATIONS { uuid id PK string name string postal_code string country } ``` **Privacy-Hinweis:** Conversations und Messages haben keine direkten User-Referenzen - nur Hashes! --- ## 1. Data Models (Collections) ### 1.1 listings (Anzeigen) Die Haupt-Collection für alle Kleinanzeigen. | 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:** ``` Settings > Data Model > + Create Collection Name: listings Primary Key: UUID (auto-generated) ``` --- ### 1.2 categories (Kategorien) Hierarchische Kategorien mit Übersetzungen. | 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 | |------|-----| | `languages_code` | String (de, en, fr) | | `name` | String | | `description` | Text | --- ### 1.3 conversations (Konversationen) - Metadaten-verschlüsselt E2E-verschlüsselter Chat mit **Zero-Knowledge Metadaten** - Server weiß nicht, wer mit wem chattet. | 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) - E2E-verschlüsselt | 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 | 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 | **Duplikat-Prüfung:** Frontend-seitig vor dem Erstellen prüfen, ob bereits favorisiert. --- ### 1.6 reports (Meldungen) | Feld | Typ | Interface | Einstellungen | |------|-----|-----------|---------------| | `id` | UUID | – | Primary Key | | `date_created` | DateTime | DateTime | Auto | | `reporter` | directus_users (M2O) | Many-to-One | Auto (user_created) | | `listing` | listings (M2O) | Many-to-One | Optional | | `reported_user` | directus_users (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 | 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 | --- ### 1.8 listings_files (Junction Table) Verknüpfung Listings ↔ Dateien (für mehrere Bilder pro Anzeige). | Feld | Typ | |------|-----| | `id` | Integer (auto) | | `listings_id` | Listings (M2O) | | `directus_files_id` | Files (M2O) | | `sort` | Integer | --- ## 2. User Roles ### 2.1 Rollen-Übersicht | Rolle | Beschreibung | App Access | Admin Access | |-------|--------------|------------|--------------| | **Administrator** | Vollzugriff | ✅ | ✅ | | **Moderator** | Content-Moderation | ✅ | ✅ (eingeschränkt) | | **User** | Registrierte Nutzer | ✅ | ❌ | | **Public** | Nicht angemeldet | ❌ | ❌ | ### 2.2 Rollen erstellen **Settings > Access Control > Roles > + Create Role** #### Administrator ``` Name: Administrator Description: Vollzugriff auf alle Funktionen Icon: shield Admin Access: ON App Access: ON ``` #### Moderator ``` Name: Moderator Description: Kann Inhalte moderieren und Reports bearbeiten Icon: eye Admin Access: ON (mit eingeschränkten Permissions) App Access: ON ``` #### User ``` Name: User Description: Registrierte Benutzer Icon: account_circle Admin Access: OFF App Access: ON ``` --- ## 3. Access Policies Konfiguration unter: **Settings > Access Control > [Rolle] > [Collection]** --- ### 3.1 Public (Nicht angemeldet) Pfad: Settings > Access Control > Public #### listings | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ Custom | `status` equals `published` | | Create | ❌ | – | | Update | ❌ | – | | Delete | ❌ | – | **Field Permissions (Read):** `id`, `title`, `slug`, `description`, `price`, `currency`, `price_mode`, `price_type`, `category`, `condition`, `images`, `location`, `shipping`, `date_created`, `views` #### categories | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ Custom | `status` equals `published` | **Field Permissions:** Alle Felder #### locations | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ All | – | #### directus_files | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ All | – | --- ### 3.2 User Role Permissions Pfad: Settings > Access Control > User #### listings | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ✅ Custom | `status` equals `published` OR `user_created` equals `$CURRENT_USER` | | Update | ✅ Custom | `user_created` equals `$CURRENT_USER` | | Delete | ✅ Custom | `user_created` equals `$CURRENT_USER` AND `status` in `draft, expired` | #### conversations | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ✅ All | – | | Update | ✅ All | – | **Privacy-Hinweis:** Serverseitiges Filtern nicht möglich (Zero-Knowledge). Das Frontend: 1. Lädt alle Conversations für ein Listing (`listing_id` Filter) 2. Prüft client-seitig, ob eigener Hash in `participant_hash_1` oder `participant_hash_2` ist 3. Zeigt nur passende Conversations an #### messages | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ✅ All | – | **Hinweis:** Inhalte sind E2E-verschlüsselt, daher keine serverseitige Filterung nötig. #### favorites | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ✅ Custom | `user` equals `$CURRENT_USER` | | Delete | ✅ Custom | `user` equals `$CURRENT_USER` | #### reports | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ❌ | – | | Update | ❌ | – | #### directus_files | Aktion | Erlaubt | Filter | |--------|---------|--------| | Create | ✅ All | – | | Read | ✅ All | – | | Update | ✅ Custom | `uploaded_by` equals `$CURRENT_USER` | | Delete | ✅ Custom | `uploaded_by` equals `$CURRENT_USER` | #### directus_users (eigenes Profil) | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ Custom | `id` equals `$CURRENT_USER` | | Update | ✅ Custom | `id` equals `$CURRENT_USER` | **Field Permissions:** `id`, `first_name`, `avatar`, `status` --- ### 3.3 Moderator Role Permissions Pfad: Settings > Access Control > Moderator Alle User-Permissions plus: #### listings | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ All | – | | Update | ✅ All | – | **Field Permissions (Update):** nur `status`, `admin_notes` #### reports | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ All | – | | Update | ✅ All | – | **Field Permissions (Update):** `status`, `admin_notes`, `resolved_by`, `resolved_at` #### directus_users (andere User sehen) | Aktion | Erlaubt | Filter | |--------|---------|--------| | Read | ✅ All | – | **Field Permissions (Read):** nur `id`, `first_name`, `avatar`, `status` --- ## 4. Flows (Automatisierungen) ### 4.1 Auto-Slug für Listings **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 1: Run Script module.exports = async function(data) { const title = data.title; const slug = title .toLowerCase() .replace(/[äöüß]/g, match => ({ä:'ae',ö:'oe',ü:'ue',ß:'ss'}[match])) .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); // Timestamp für Eindeutigkeit return { slug: `${slug}-${Date.now().toString(36)}` }; } ``` ``` // Operation 2: Update Data Collection: listings ID: {{$trigger.key}} Payload: { "slug": "{{$last.slug}}" } ``` **Ergebnis:** Title "iPhone 15 Pro!" → Slug `iphone-15-pro-m1abc2def` --- ### 4.2 Auto-Set Expiry Date Setzt automatisch ein Ablaufdatum 30 Tage in der Zukunft. **Flow-Aufbau:** 1. **Trigger:** `items.create` auf `listings` 2. **Operation 1:** Run Script (Datum berechnen) 3. **Operation 2:** Update Data (Datum speichern) ```javascript // Operation 1: Run Script module.exports = async function(data) { const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 30); return { expires_at: expiresAt.toISOString() }; } ``` ``` // Operation 2: Update Data Collection: listings ID: {{$trigger.key}} Payload: { "expires_at": "{{$last.expires_at}}" } ``` **Ergebnis:** Listing am 01.01. erstellt → `expires_at` = 31.01. --- ### 4.3 Increment View Counter Erhöht den View-Counter bei jedem Aufruf einer Listing-Detailseite. **Flow-Aufbau:** 1. **Trigger:** Webhook (vom Frontend aufgerufen) 2. **Operation 1:** Run Script (Counter erhöhen) ``` // Trigger: Webhook Name: increment-views Method: POST URL: /flows/trigger/increment-views ``` ```javascript // Operation 1: Run Script module.exports = async function(data, { database }) { if (!data.listing_id) return; await database('listings') .where('id', data.listing_id) .increment('views', 1); return { success: true }; } ``` **Frontend-Aufruf:** ```javascript fetch('/flows/trigger/increment-views', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ listing_id: 'uuid-here' }) }); ``` --- ### 4.4 Auto-Expire Listings (Scheduled) Markiert abgelaufene Listings automatisch als `expired`. **Flow-Aufbau:** 1. **Trigger:** Schedule (Cron) 2. **Operation 1:** Run Script (Abgelaufene updaten) ``` // Trigger: Schedule Type: Schedule (Cron) Cron: 0 0 * * * (täglich um 00:00 UTC) ``` ```javascript // Operation 1: Run Script module.exports = async function(data, { database }) { const now = new Date().toISOString(); const updated = await database('listings') .where('expires_at', '<', now) .where('status', 'published') .update({ status: 'expired' }); return { expired_count: updated }; } ``` **Ergebnis:** Alle Listings mit `expires_at` in der Vergangenheit werden auf `status: expired` gesetzt. --- ## 5. Einstellungen ### 5.1 Project Settings **Settings > Project Settings** ``` Project Name: kashilo.com Project URL: https://kashilo.com Project Color: #555555 ``` ### 5.2 CORS Settings **Option 1: docker-compose.yml** ```yaml services: directus: image: directus/directus:latest environment: CORS_ENABLED: "true" CORS_ORIGIN: "https://kashilo.com,https://www.kashilo.com,http://localhost:8080" CORS_METHODS: "GET,POST,PATCH,DELETE" CORS_ALLOWED_HEADERS: "Content-Type,Authorization" CORS_CREDENTIALS: "true" ``` **Option 2: .env Datei + Docker Compose** ```yaml # docker-compose.yml services: directus: env_file: .env ``` ```env # .env CORS_ENABLED=true CORS_ORIGIN=https://kashilo.com,https://www.kashilo.com,http://localhost:8080 CORS_METHODS=GET,POST,PATCH,DELETE CORS_ALLOWED_HEADERS=Content-Type,Authorization CORS_CREDENTIALS=true ``` ### 5.3 Auth Settings Konfiguration wie CORS via **docker-compose.yml** oder **.env Datei**: ```yaml # docker-compose.yml environment: ACCESS_TOKEN_TTL: "15m" REFRESH_TOKEN_TTL: "7d" AUTH_PASSWORD_POLICY: "/^.{36,}$/" # UUID = 36 chars PUBLIC_REGISTRATION: "true" ``` Oder in `.env`: ```env ACCESS_TOKEN_TTL=15m REFRESH_TOKEN_TTL=7d AUTH_PASSWORD_POLICY=/^.{36,}$/ PUBLIC_REGISTRATION=true ``` ### 5.4 File Storage Konfiguration via **docker-compose.yml** oder **.env Datei**: **Option 1: Lokal (einfach)** ```yaml # docker-compose.yml environment: STORAGE_LOCATIONS: "local" STORAGE_LOCAL_ROOT: "./uploads" volumes: - ./uploads:/directus/uploads ``` **Option 2: Hetzner Object Storage (empfohlen)** ```yaml # docker-compose.yml environment: STORAGE_LOCATIONS: "s3" STORAGE_S3_DRIVER: "s3" STORAGE_S3_KEY: "your-access-key" STORAGE_S3_SECRET: "your-secret-key" STORAGE_S3_BUCKET: "kashilo-files" STORAGE_S3_REGION: "fsn1" # oder nbg1 STORAGE_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" # oder nbg1 ``` **Option 3: Cloudflare R2** ```yaml # docker-compose.yml environment: STORAGE_LOCATIONS: "s3" STORAGE_S3_DRIVER: "s3" STORAGE_S3_KEY: "your-access-key" STORAGE_S3_SECRET: "your-secret-key" STORAGE_S3_BUCKET: "kashilo-files" STORAGE_S3_REGION: "auto" STORAGE_S3_ENDPOINT: "https://xxx.r2.cloudflarestorage.com" ``` ### 5.5 Rate Limiting Konfiguration via **docker-compose.yml** oder **.env Datei**: ```yaml # docker-compose.yml environment: RATE_LIMITER_ENABLED: "true" RATE_LIMITER_STORE: "memory" # oder "redis" für Multi-Instance RATE_LIMITER_POINTS: "100" # Max Requests RATE_LIMITER_DURATION: "60" # Pro Minute ``` Oder in `.env`: ```env RATE_LIMITER_ENABLED=true RATE_LIMITER_STORE=memory RATE_LIMITER_POINTS=100 RATE_LIMITER_DURATION=60 ``` **Hinweis:** Keine E-Mail-Konfiguration nötig - kashilo.com nutzt keine E-Mails (Privacy by Design). ### 5.6 Währungsumrechnung & Preismodus Implementiert im Frontend-Service: **`js/services/currency.js`** **Features:** - Kraken API für Echtzeit-Kurse - 5 Minuten Client-Cache - Unterstützte Währungen: XMR, EUR, CHF, USD, GBP, JPY - Zwei Preismodi: `fiat` (Fiat-fix) und `xmr` (XMR-fix) **Nutzung:** ```javascript import { getXmrRates, formatPrice } from './services/currency.js'; // Kurse laden const rates = await getXmrRates(); // Preis formatieren const listing = { price: 100, currency: 'EUR', price_mode: 'fiat' }; const display = formatPrice(listing, rates); // → { primary: "€ 100,00", secondary: "≈ 0.6667 XMR", xmrAmount: 0.6667 } ``` **Preismodi:** | 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 | **Beispiele (bei 1 XMR = 150 EUR):** | Listing | Anzeige | |---------|---------| | 100 EUR, mode=`fiat` | **€ 100,00** ≈ 0.6667 XMR | | 0.5 XMR, mode=`xmr` | **0.5000 XMR** ≈ € 75,00 | --- ## 6. Frontend-Integration ### 6.1 Service einbinden ```javascript import { directus, DirectusError } from './services/directus.js'; // Login try { await directus.login('user@example.com', 'password'); console.log('Logged in!'); } catch (error) { if (error instanceof DirectusError) { console.error('Login failed:', error.message); } } // Listings laden const { items, meta } = await directus.getListings({ limit: 20, page: 1 }); // Listing erstellen const newListing = await directus.createListing({ title: 'iPhone 15 Pro', description: 'Neuwertig, OVP', price: 0.5, currency: 'XMR', category: 'electronics-id', condition: 'like_new' }); ``` ### 6.2 Bilder anzeigen ```javascript // Thumbnail URL const thumbUrl = directus.getThumbnailUrl(fileId, 300); // Optimiertes Bild const imageUrl = directus.getFileUrl(fileId, { width: 800, height: 600, fit: 'cover', quality: 80, format: 'webp' }); ``` ### 6.3 Auth State prüfen ```javascript if (directus.isAuthenticated()) { const user = await directus.getCurrentUser(); console.log('Logged in as:', user.email); } else { // Redirect to login } ``` --- ## 7. Initiale Daten ### 7.1 Kategorien (Beispiel) ```json [ { "name": "Elektronik", "slug": "elektronik", "icon": "devices" }, { "name": "Fahrzeuge", "slug": "fahrzeuge", "icon": "directions_car" }, { "name": "Immobilien", "slug": "immobilien", "icon": "home" }, { "name": "Mode & Accessoires", "slug": "mode", "icon": "checkroom" }, { "name": "Haus & Garten", "slug": "haus-garten", "icon": "yard" }, { "name": "Freizeit & Hobby", "slug": "freizeit", "icon": "sports_esports" }, { "name": "Jobs", "slug": "jobs", "icon": "work" }, { "name": "Dienstleistungen", "slug": "dienstleistungen", "icon": "handyman" }, { "name": "Sonstiges", "slug": "sonstiges", "icon": "more_horiz" } ] ``` ### 7.2 Sub-Kategorien Elektronik ```json [ { "name": "Smartphones", "slug": "smartphones", "parent": "elektronik-id" }, { "name": "Computer & Laptops", "slug": "computer", "parent": "elektronik-id" }, { "name": "TV & Audio", "slug": "tv-audio", "parent": "elektronik-id" }, { "name": "Foto & Video", "slug": "foto-video", "parent": "elektronik-id" }, { "name": "Gaming", "slug": "gaming", "parent": "elektronik-id" }, { "name": "Zubehör", "slug": "zubehoer", "parent": "elektronik-id" } ] ``` --- ## 8. Checkliste - [ ] Collections erstellen (listings, categories, etc.) - [ ] Felder konfigurieren (Typen, Validierung) - [ ] Rollen anlegen (Admin, Moderator, User) - [ ] Access Policies für jede Rolle setzen - [ ] Public Access für Listings/Categories aktivieren - [ ] Flows für Auto-Slug, Expiry, etc. erstellen - [ ] CORS für Frontend-Domain konfigurieren - [ ] Email-Transport einrichten - [ ] Kategorien importieren - [ ] Test-User erstellen - [ ] Frontend mit `directus.js` Service verbinden --- ## 9. Anonyme Authentifizierung (UUID-basiert) Für maximale Privatsphäre nutzt kashilo.com ein UUID-basiertes Login-System ohne echte E-Mail-Adressen. ### 9.1 Konzept ``` ┌─────────────────────────────────────────────────────────────┐ │ User klickt "Account erstellen" │ │ ↓ │ │ Client generiert UUID v4: │ │ f47ac10b-58cc-4372-a567-0e02b2c3d479 │ │ ↓ │ │ Directus erhält: │ │ • E-Mail: f47ac10b-58cc-4372-a567-0e02b2c3d479@kashilo.com │ │ • Passwort: f47ac10b-58cc-4372-a567-0e02b2c3d479 │ │ ↓ │ │ User speichert UUID → fertig │ └─────────────────────────────────────────────────────────────┘ ``` ### 9.2 Vorteile | Aspekt | Beschreibung | |--------|--------------| | **Privatsphäre** | Keine echten E-Mail-Adressen, keine persönlichen Daten | | **Einfachheit** | User merkt sich nur eine UUID | | **Sicherheit** | 122 bit Entropie, Argon2-Hashing durch Directus | | **Anonymität** | Kein Bezug zur Identität möglich | ### 9.3 Tradeoffs | Pro | Contra | |-----|--------| | Keine echten Daten nötig | UUID verloren = Account verloren | | Nicht rückverfolgbar | Kein Passwort-Reset möglich | | Simples UX (1 Secret) | User muss UUID sicher aufbewahren | ### 9.4 Frontend-Implementation ```javascript // Account erstellen async function createAnonymousAccount() { const uuid = crypto.randomUUID(); const email = `${uuid}@kashilo.com`; const password = uuid; // Bei Directus registrieren await directus.register(email, password); // UUID dem User anzeigen zum Speichern showUuidToUser(uuid); return uuid; } // Login async function login(uuid) { const email = `${uuid}@kashilo.com`; await directus.login(email, uuid); } ``` ### 9.5 UX-Flow 1. **Registrierung:** - User klickt "Account erstellen" - System generiert UUID, zeigt sie prominent an - User kopiert UUID (Button "Kopieren") - Hinweis: "Speichere diese ID sicher - sie ist dein einziger Zugang!" 2. **Login:** - User gibt UUID ein (ein Feld) - System baut E-Mail + Passwort daraus - Fertig 3. **Recovery:** - Nicht möglich (by design) - Optional: QR-Code zum Offline-Speichern anbieten ### 9.6 Directus-Konfiguration ```env # Public Registration erlauben PUBLIC_REGISTRATION=true # Keine E-Mail-Verifizierung (fake E-Mails) AUTH_EMAIL_VERIFY=false # Kein Passwort-Reset (nicht möglich mit fake E-Mails) AUTH_PASSWORD_RESET=false ``` --- ## 10. Sicherheits- & Privacy-Hinweise ### 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)