Files
kashilo/docs/DIRECTUS-SETUP.md

1012 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: Cloudflare R2 / S3 (empfohlen für Produktion)**
```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)