# Directus Schema Documentation **Directus Version:** 11.14.1 **Database:** PostgreSQL **API Endpoint:** https://api.kashilo.com ## Collections Overview | Collection | Beschreibung | |------------|--------------| | `listings` | Haupttabelle für Anzeigen | | `listings_files` | Junction-Table für Listing-Bilder | | `categories` | Kategorien mit Übersetzungen | | `categories_translations` | Kategorie-Übersetzungen (i18n) | | `locations` | Standorte für Anzeigen | | `languages` | Verfügbare Sprachen | | `conversations` | Chat-Konversationen | | `messages` | Chat-Nachrichten | | `favorites` | Favoriten (User-Listing Relation) | | `reports` | Meldungen/Beschwerden | --- ## listings Haupttabelle für alle Anzeigen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `status` | string | `draft`, `published`, `archived` | | `sort` | integer | Sortierung | | `title` | string | Titel der Anzeige | | `slug` | string | URL-freundlicher Titel | | `description` | text | Beschreibung | | `price` | decimal | Preis | | `currency` | string | Währung: `EUR`, `CHF`, `USD`, `XMR` | | `price_mode` | string | `fiat` oder `xmr` (Referenzwährung) | | `price_type` | string | `fixed`, `negotiable`, `free` | | `condition` | string | `new`, `like_new`, `good`, `fair`, `poor` | | `shipping` | boolean | Versand möglich | | `shipping_cost` | decimal | Versandkosten | | `views` | integer | Aufrufzähler | | `expires_at` | datetime | Ablaufdatum | | `monero_address` | string | XMR-Adresse für Zahlung | | `contact_public_key` | text | NaCl Public Key für E2E-Chat (pro Listing) | | `verified` | boolean | `false` default — Verifikation abgeschlossen | | `verification_code` | string(6) | 6-stelliger Klartext-Code (für Käufer-Vergleich mit Foto) | | `verification_image` | UUID | FK → directus_files (Verifikationsfoto) | | `verification_date` | datetime | Zeitpunkt der Verifikation | | `date_created` | datetime | Erstellungsdatum | | `date_updated` | datetime | Änderungsdatum | | `user_created` | UUID | Ersteller (FK → directus_users) | | `category` | UUID | Kategorie (FK → categories) | | `location` | UUID | Standort (FK → locations) | | `images` | o2m | Bilder (→ listings_files) | --- ## listings_files Junction-Table für Listing-Bilder (Many-to-Many). | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | integer | Primary Key | | `listings_id` | UUID | FK → listings | | `directus_files_id` | UUID | FK → directus_files | | `sort` | integer | Sortierung der Bilder | --- ## categories Kategorien mit hierarchischer Struktur. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `status` | string | `draft`, `published`, `archived` | | `sort` | integer | Sortierung | | `name` | string | Kategorie-Name (Fallback) | | `slug` | string | URL-freundlicher Name | | `icon` | string | Icon (Emoji oder Icon-Name) | | `parent` | UUID | Parent-Kategorie (FK → categories) | | `translations` | o2m | Übersetzungen (→ categories_translations) | --- ## categories_translations Übersetzungen für Kategorien. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | integer | Primary Key | | `categories_id` | UUID | FK → categories | | `languages_code` | string | Sprachcode (`de-DE`, `en-US`, `fr-FR`, `it-IT`, `es-ES`, `pt-BR`, `ru-RU`) | | `name` | string | Übersetzter Name | --- ## locations Standorte für Anzeigen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `name` | string | Ortsname | | `postal_code` | string | Postleitzahl | | `region` | string | Region/Kanton | | `country` | string | Land: `DE`, `AT`, `CH`, `FR`, `IT`, `LI` | | `latitude` | float | Breitengrad | | `longitude` | float | Längengrad | --- ## languages Verfügbare Sprachen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `code` | string | Primary Key, z.B. `de-DE`, `en-US`, `fr-FR`, `it-IT`, `es-ES`, `pt-BR`, `ru-RU` | | `name` | string | Sprachname | | `direction` | string | Textrichtung: `ltr` oder `rtl` | --- ## conversations Zero-Knowledge Chat-Konversationen zwischen anonymen Usern. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `listing_id` | UUID | FK → listings | | `participant_hash_1` | string(64) | SHA-256 Hash des ersten Teilnehmers (Käufer) | | `participant_hash_2` | string(64) | SHA-256 Hash des zweiten Teilnehmers (Verkäufer) | | `public_key_1` | text | NaCl Public Key Teilnehmer 1 | | `public_key_2` | text | NaCl Public Key Teilnehmer 2 | | `status` | string | `active`, `closed` | | `buyer_user` | UUID | FK → directus_users, Buyer's Directus user ID | | `date_created` | datetime | Erstellungsdatum | | `date_updated` | datetime | Letzte Nachricht | **Hinweis:** Die `participant_hash_*` Felder ermöglichen anonyme Zuordnung ohne Directus-User-Accounts. Das `buyer_user` Feld speichert die Directus-User-ID des Käufers und wird bei Conversation-Erstellung gesetzt. --- ## messages E2E-verschlüsselte Nachrichten in Konversationen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `conversation` | UUID | FK → conversations | | `sender_hash` | string(64) | SHA-256 Hash des Senders | | `content_encrypted` | text | NaCl-verschlüsselter Inhalt | | `nonce` | string | Nonce für Entschlüsselung | | `type` | string | `text`, `image`, `system` | | `date_created` | datetime | Zeitstempel | **Hinweis:** Nachrichten sind client-seitig E2E-verschlüsselt. Server sieht nur Ciphertext. --- ## favorites User-Favoriten. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `user` | UUID | FK → directus_users | | `listing` | UUID | FK → listings | | `date_created` | datetime | Hinzugefügt am | --- ## directus_users (Custom Fields) Zusätzliche Felder für User-Einstellungen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `preferred_currency` | string | Bevorzugte Währung: `USD`, `EUR`, `CHF` (Default: `USD`) | | `preferred_locale` | string | Bevorzugte Sprache: `de-DE`, `en-US`, `fr-FR`, `it-IT`, `es-ES`, `pt-BR`, `ru-RU` | **Hinweis:** Diese Felder müssen in Directus unter Settings → Data Model → directus_users angelegt werden. --- ## reports Meldungen von Anzeigen. | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | UUID | Primary Key | | `listing` | UUID | FK → listings | | `reporter` | UUID | FK → directus_users | | `reason` | string | Grund der Meldung | | `description` | text | Details | | `status` | string | `pending`, `reviewed`, `resolved` | | `date_created` | datetime | Meldungsdatum | --- ## Public Role Permissions | Collection | Read | Create | Update | Delete | Hinweise | |------------|:----:|:------:|:------:|:------:|----------| | `listings` | ✓ | ✓ | ✓ | - | Siehe Details unten | | `listings_files` | ✓ | ✓ | - | - | Für Bilder-Upload | | `directus_files` | ✓ | ✓ | - | - | Asset-Upload | | `categories` | ✓ | - | - | - | Nur `status=published` | | `categories_translations` | ✓ | - | - | - | Für i18n | | `locations` | ✓ | ✓ | - | - | User kann neue Orte anlegen | | `languages` | ✓ | - | - | - | Für Sprach-Auswahl | | `conversations` | - | - | - | - | **Nur User-Rolle** (s.u.) | | `messages` | - | - | - | - | **Nur User-Rolle** (s.u.) | | `favorites` | ✓ | ✓ | - | ✓ | Nur eigene | | `reports` | - | ✓ | - | - | Nur erstellen | ### Listings Update-Berechtigungen — Public Role (Detail) **Custom Filter:** ```json { "user_created": { "_eq": "$CURRENT_USER" } } ``` **Field Permissions (Update):** - `title`, `slug`, `description` - `price`, `currency`, `price_mode`, `price_type` - `category`, `condition`, `location` - `shipping`, `shipping_cost` - `monero_address` - `contact_public_key` - `images` - `views` (geschützt durch Flow) - `verified`, `verification_code`, `verification_image`, `verification_date` **Read Filter:** ```json { "status": { "_eq": "published" } } ``` **Hinweis:** Die Felder `paid_at`, `payment_status` und `btcpay_invoice_id` sind in der Public-Rolle **nicht** lesbar. ### User Role: Listings (zusätzliche Felder) Eingeloggte User haben zusätzlich zu den Public-Feldern Zugriff auf: **Read (zusätzlich):** - `paid_at`, `payment_status`, `btcpay_invoice_id` **Update:** - Gleiche Felder wie Public Update - Filter: `user_created = $CURRENT_USER` **Hinweis:** `paid_at` und `payment_status` dürfen NICHT in der Public-Rolle lesbar sein, da sonst `getListing()` für nicht-eingeloggte Besucher fehlschlägt. --- ## Directus Flows ### Increment Listing Views **Zweck:** Sichert ab, dass `views` nur inkrementiert (nicht beliebig gesetzt) werden kann. | Schritt | Typ | Beschreibung | |---------|-----|--------------| | 1. Trigger | Action (Non-Blocking) | `items.update` auf `listings` | | 2. Condition | Filter Rule | Prüft ob `views` im Payload vorhanden | | 3. Operation | Run Script | Prüft ob **nur** `views` geändert wurde | | 4. Condition | Filter Rule | Prüft Script-Ergebnis | **Schritt 2 - Condition Rule (views vorhanden):** ```json { "$trigger": { "payload": { "views": { "_nnull": true } } } } ``` **Schritt 3 - Run Script (nur views im Payload):** ```javascript module.exports = async function(data) { const keys = Object.keys(data.$trigger.payload) return keys.length === 1 && keys[0] === 'views' ? 1 : 0 } ``` **Schritt 4 - Condition Rule (Script-Ergebnis prüfen):** ```json { "$last": { "_eq": 1 } } ``` **Hinweis:** Ohne diese Absicherung könnte jeder `views` auf beliebige Werte setzen. ### Archive Expired Listings **Zweck:** Archiviert abgelaufene Listings automatisch. | Schritt | Typ | Beschreibung | |---------|-----|--------------| | 1. Trigger | Schedule `*/15 * * * *` | Alle 15 Minuten | | 2. Operation | Update Data | `status → archived` wenn `expires_at < NOW` | ### ~~Notify: Listing Published~~ (Deaktiviert) **Status:** Deaktiviert — wird jetzt direkt von `btcpay-webhook.php` gehandhabt (mit Duplikat-Check: prüft auf existierende Notification bevor eine neue erstellt wird, verarbeitet nur `InvoiceSettled`). ### Notify: New Message **Zweck:** Erstellt eine Benachrichtigung für den Empfänger einer neuen Chat-Nachricht. | Schritt | Typ | Beschreibung | |---------|-----|--------------| | 1. Trigger | Event Hook | `items.create` auf `messages` | | 2. Read Data | Read Data | Conversation laden (anhand `conversation` ID aus Payload) | | 3. Read Data | Read Data | Listing laden (anhand `listing_id` aus Conversation) | | 4. Run Script | Run Script | Empfänger ermitteln (`sender_hash` ≠ `participant_hash_1/2` → anderer ist Empfänger) | | 5. Condition | Filter Rule | Prüft ob Empfänger ermittelt wurde | | 6. Create Data | Create Data | Notification erstellen für Empfänger | **Wichtig:** Die Read Data und Create Data Operations benötigen **`$full`** Permissions (nicht `$trigger`), da sie auf Collections zugreifen die nicht im Trigger-Context verfügbar sind. --- ## User Role Permissions (Chat) Conversations und Messages sind **nicht** über die Public-Rolle zugänglich. Nur eingeloggte User (User-Rolle) dürfen diese Collections nutzen. ### conversations (User-Rolle) | Aktion | Erlaubt | Filter / Einschränkung | |--------|:-------:|------------------------| | Read | ✓ | Alle (Client filtert via `participant_hash`) | | Create | ✓ | Felder inkl. `buyer_user` | | Update | ✓ | Nur `status`, `buyer_user` Felder, kein Row-Filter | | Delete | - | Nicht erlaubt | **Hinweis:** Im Zero-Knowledge-Design gibt es kein server-verifizierbares Feld, das beide Teilnehmer identifiziert (`participant_hash` ist nicht an einen Directus-User gebunden). Deshalb kann kein `$CURRENT_USER`-Filter für Read/Update gesetzt werden. Die Absicherung erfolgt durch: (1) Authentifizierungspflicht (Public hat keinen Zugriff), (2) Client-seitige Filterung via `participant_hash`, (3) Update nur auf `status` und `buyer_user` Felder beschränkt, (4) Conversation-UUIDs sind nicht erratbar. ### messages (User-Rolle) | Aktion | Erlaubt | Filter / Einschränkung | |--------|:-------:|------------------------| | Read | ✓ | Alle (Inhalte sind E2E-verschlüsselt) | | Create | ✓ | Keine Einschränkung | | Update | - | Nicht erlaubt | | Delete | - | Nicht erlaubt | **Sicherheitsmodell:** - Nachrichten sind client-seitig E2E-verschlüsselt — Server sieht nur Ciphertext - Ohne Private Key kann niemand Nachrichten entschlüsseln - Public-Rolle hat **keinen** Zugriff auf `conversations` und `messages` - Nur eingeloggte User können Conversations erstellen und Nachrichten senden