# 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) |
| `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` |
| `date_created` | datetime | Erstellungsdatum |
| `date_updated` | datetime | Letzte Nachricht |

**Hinweis:** Die `participant_hash_*` Felder ermöglichen anonyme Zuordnung ohne Directus-User-Accounts.

---

## 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 (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)

**Read Filter:**
```json
{ "status": { "_eq": "published" } }
```

---

## 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.

---

## 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 | ✓ | Keine Einschränkung |
| Update | ✓ | Nur `status` Feld, 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`-Feld 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
