# Directus Setup für dgray.io

Komplette Anleitung zur Einrichtung von Directus als Backend für die dgray Kleinanzeigen-PWA.

**API URL**: https://api.dgray.io/

---

## 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: dgray.io
Project URL: https://dgray.io
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://dgray.io,https://www.dgray.io,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://dgray.io,https://www.dgray.io,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: "dgray-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: "dgray-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 - dgray.io 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 dgray.io 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@dgray.io │
│  • 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}@dgray.io`;
    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}@dgray.io`;
    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)
