improve DIRECTUS-SETUP

This commit is contained in:
2026-01-31 11:26:34 +01:00
parent 7865435e8c
commit 51346bdd7d

View File

@@ -27,13 +27,10 @@ Komplette Anleitung zur Einrichtung von Directus als Backend für die dgray Klei
erDiagram
USERS ||--o{ LISTINGS : creates
USERS ||--o{ FAVORITES : has
USERS ||--o{ CONVERSATIONS : participates
USERS ||--o{ MESSAGES : sends
USERS ||--o{ REPORTS : submits
LISTINGS ||--o{ LISTINGS_FILES : has
LISTINGS ||--o{ FAVORITES : receives
LISTINGS ||--o{ CONVERSATIONS : has
LISTINGS }o--|| CATEGORIES : belongs_to
LISTINGS }o--o| LOCATIONS : located_at
@@ -66,18 +63,21 @@ erDiagram
CONVERSATIONS {
uuid id PK
uuid listing FK
uuid buyer FK
uuid seller FK
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
uuid sender FK
text content
datetime read_at
string sender_hash
text content_encrypted
string nonce
string type
}
FAVORITES {
@@ -102,6 +102,8 @@ erDiagram
}
```
**Privacy-Hinweis:** Conversations und Messages haben keine direkten User-Referenzen - nur Hashes!
---
## 1. Data Models (Collections)
@@ -110,31 +112,31 @@ erDiagram
Die Haupt-Collection für alle Kleinanzeigen.
| Feld | Typ | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key, auto-generated |
| `status` | String (Dropdown) | `draft`, `published`, `sold`, `expired`, `deleted` |
| `sort` | Integer | Für manuelle Sortierung |
| `user_created` | User (M2O) | Auto, Read-only |
| `date_created` | DateTime | Auto, Read-only |
| `user_updated` | User (M2O) | Auto |
| `date_updated` | DateTime | Auto |
| `title` | String | Required, max 100 chars |
| `slug` | String | Unique, auto-generated from title |
| `description` | Text (WYSIWYG) | Required |
| `price` | Decimal | Required, precision 10, scale 2 |
| `currency` | String | Default: `XMR`, Options: `XMR`, `EUR` |
| `price_type` | String | `fixed`, `negotiable`, `free`, `on_request` |
| `category` | Categories (M2O) | Required |
| `condition` | String | `new`, `like_new`, `good`, `fair`, `poor` |
| `images` | Files (M2M) | Junction: `listings_files` |
| `location` | Locations (M2O) | Optional |
| `shipping` | Boolean | Versand möglich? |
| `shipping_cost` | Decimal | Optional |
| `views` | Integer | Default: 0 |
| `expires_at` | DateTime | Auto-set, 30 Tage nach Erstellung |
| `monero_address` | String | Für Direktzahlung |
| `contact_method` | String | `chat`, `email`, `both` |
| 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:**
```
@@ -149,16 +151,16 @@ Primary Key: UUID (auto-generated)
Hierarchische Kategorien mit Übersetzungen.
| Feld | Typ | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `status` | String | `draft`, `published` |
| `sort` | Integer | Für Sortierung |
| `name` | String | Required (Fallback-Name) |
| `slug` | String | Unique |
| `icon` | String | Icon-Name (z.B. `laptop`, `car`) |
| `parent` | Categories (M2O) | Self-referencing |
| `translations` | Translations | Junction: `categories_translations` |
| 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 |
@@ -169,45 +171,67 @@ Hierarchische Kategorien mit Übersetzungen.
---
### 1.3 conversations (Konversationen)
### 1.3 conversations (Konversationen) - Metadaten-verschlüsselt
Chat zwischen Käufer und Verkäufer.
E2E-verschlüsselter Chat mit **Zero-Knowledge Metadaten** - Server weiß nicht, wer mit wem chattet.
| Feld | Typ | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `date_created` | DateTime | Auto |
| `date_updated` | DateTime | Auto |
| `listing` | Listings (M2O) | Required |
| `buyer` | User (M2O) | Wer die Konversation gestartet hat |
| `seller` | User (M2O) | Auto from listing.user_created |
| `status` | String | `active`, `archived`, `blocked` |
| `messages` | Messages (O2M) | |
| 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)
### 1.4 messages (Nachrichten) - E2E-verschlüsselt
| Feld | Typ | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `date_created` | DateTime | Auto |
| `conversation` | Conversations (M2O) | Required |
| `sender` | User (M2O) | Auto (user_created) |
| `content` | Text | Required, max 2000 chars |
| `read_at` | DateTime | Null = ungelesen |
| `type` | String | `text`, `offer`, `system` |
| 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 | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `date_created` | DateTime | Auto |
| `user` | User (M2O) | Auto (user_created) |
| `listing` | Listings (M2O) | Required |
| 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 |
**Unique Constraint:** `user` + `listing`
@@ -215,33 +239,33 @@ Chat zwischen Käufer und Verkäufer.
### 1.6 reports (Meldungen)
| Feld | Typ | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `date_created` | DateTime | Auto |
| `reporter` | User (M2O) | Auto |
| `listing` | Listings (M2O) | Optional |
| `reported_user` | User (M2O) | Optional |
| `reason` | String | `spam`, `fraud`, `inappropriate`, `illegal`, `other` |
| `details` | Text | |
| `status` | String | `pending`, `reviewed`, `resolved`, `dismissed` |
| `admin_notes` | Text | Nur für Admins sichtbar |
| `resolved_by` | User (M2O) | |
| `resolved_at` | DateTime | |
| Feld | Typ | Interface | Einstellungen |
|------|-----|-----------|---------------|
| `id` | UUID | | Primary Key |
| `date_created` | DateTime | DateTime | Auto |
| `reporter` | User (M2O) | User | Auto |
| `listing` | Listings (M2O) | Many-to-One | Optional |
| `reported_user` | User (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 | Einstellungen |
|------|-----|---------------|
| `id` | UUID | Primary Key |
| `name` | String | Stadt/Ort |
| `postal_code` | String | PLZ |
| `region` | String | Bundesland/Kanton |
| `country` | String | Default: `DE` |
| `latitude` | Float | Optional, für Kartenansicht |
| `longitude` | Float | Optional |
| 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 |
---
@@ -500,8 +524,14 @@ Zusätzlich zu User-Permissions:
**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: Run Script
// Operation 1: Run Script
module.exports = async function(data) {
const title = data.title;
const slug = title
@@ -510,11 +540,19 @@ module.exports = async function(data) {
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
// Timestamp für Eindeutigkeit
return { slug: `${slug}-${Date.now().toString(36)}` };
}
```
**Operation:** Update Data → `listings` mit Ergebnis
```
// Operation 2: Update Data
Collection: listings
ID: {{$trigger.key}}
Payload: { "slug": "{{$last.slug}}" }
```
**Ergebnis:** Title "iPhone 15 Pro!" → Slug `iphone-15-pro-m1abc2def`
---
@@ -580,19 +618,6 @@ module.exports = async function(data, { database }) {
---
### 4.6 Welcome Email bei Registrierung
**Trigger:** `users.create`
**Operation:** Send Email
```
To: {{$trigger.email}}
Subject: Willkommen bei dgray.io
Template: welcome-email
```
---
## 5. Einstellungen
### 5.1 Project Settings
@@ -656,17 +681,94 @@ RATE_LIMITER_POINTS=100
RATE_LIMITER_DURATION=60
```
### 5.6 Email Settings
**Hinweis:** Keine E-Mail-Konfiguration nötig - dgray.io nutzt keine E-Mails (Privacy by Design).
```env
EMAIL_FROM=noreply@dgray.io
EMAIL_TRANSPORT=smtp
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=xxx
EMAIL_SMTP_PASSWORD=xxx
EMAIL_SMTP_SECURE=false
### 5.6 Währungsumrechnung (Kraken API)
Für die Anzeige von Fiat-Preisen in XMR wird die Kraken API genutzt.
**API Endpoint:**
```
https://api.kraken.com/0/public/Ticker?pair=XMRUSD,XMREUR,XMRGBP,XMRCHF,XMRJPY
```
**Frontend-Implementation:**
```javascript
const KRAKEN_API = 'https://api.kraken.com/0/public/Ticker';
const PAIRS = {
USD: 'XMRUSD',
EUR: 'XMREUR',
GBP: 'XMRGBP',
CHF: 'XMRCHF',
JPY: 'XMRJPY'
};
async function getXmrRates() {
const pairs = Object.values(PAIRS).join(',');
const response = await fetch(`${KRAKEN_API}?pair=${pairs}`);
const data = await response.json();
const rates = {};
for (const [currency, pair] of Object.entries(PAIRS)) {
const ticker = data.result[pair];
if (ticker) {
rates[currency] = parseFloat(ticker.c[0]); // Last trade price
}
}
return rates;
}
function convertToXmr(amount, currency, rates) {
if (currency === 'XMR') return amount;
return amount / rates[currency];
}
// Beispiel: 100 EUR in XMR
// const rates = await getXmrRates();
// const xmrAmount = convertToXmr(100, 'EUR', rates);
```
**Caching:** Rates werden client-side für 5 Minuten gecached.
### 5.7 Preismodus (Fiat vs. XMR)
Listings können in zwei Modi erstellt werden:
| 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 |
**Frontend-Anzeige:**
```javascript
function displayPrice(listing, rates) {
const { price, currency, price_mode } = listing;
if (price_mode === 'xmr' || currency === 'XMR') {
// XMR ist der Referenzpreis
const xmrPrice = currency === 'XMR' ? price : price / rates[currency];
return {
primary: `${xmrPrice.toFixed(4)} XMR`,
secondary: currency !== 'XMR' ? `${price} ${currency}` : null
};
} else {
// Fiat ist der Referenzpreis
const xmrEquivalent = price / rates[currency];
return {
primary: `${price} ${currency}`,
secondary: `${xmrEquivalent.toFixed(4)} XMR`
};
}
}
```
**Beispiele:**
| Listing | Anzeige (bei 1 XMR = 150 EUR) |
|---------|-------------------------------|
| 100 EUR, mode=`fiat` | **100 EUR** ≈ 0.6667 XMR |
| 100 EUR, mode=`xmr` | **0.6667 XMR** ≈ 100 EUR |
| 0.5 XMR, mode=`xmr` | **0.5 XMR** ≈ 75 EUR |
---
@@ -877,11 +979,32 @@ AUTH_PASSWORD_RESET=false
---
## 10. Sicherheitshinweise
## 10. Sicherheits- & Privacy-Hinweise
1. **Monero-Adressen** werden nur dem Listing-Ersteller und authentifizierten Nutzern angezeigt
2. **Keine echten E-Mails** - UUID-basierte Authentifizierung
3. **Rate Limiting** für API-Endpunkte aktivieren
4. **Bilder-Upload** auf max. 10MB und erlaubte Typen beschränken
5. **XSS-Schutz** für WYSIWYG-Felder in Directus aktivieren
6. **UUID-Warnung** bei Registrierung: User muss UUID sicher speichern
### 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)