Files
kashilo/docs/DIRECTUS-SETUP.md

1011 lines
26 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 |
**Unique Constraint:** `user` + `listing`
---
### 1.6 reports (Meldungen)
| 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 | 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
### 3.1 Public (Nicht angemeldet)
**listings:**
```json
{
"read": {
"_and": [
{ "status": { "_eq": "published" } }
]
},
"fields": ["id", "title", "slug", "description", "price", "currency", "price_type", "category", "condition", "images", "location", "shipping", "date_created", "views"]
}
```
**categories:**
```json
{
"read": {
"status": { "_eq": "published" }
},
"fields": "*"
}
```
**locations:**
```json
{
"read": true,
"fields": "*"
}
```
**directus_files:**
```json
{
"read": true
}
```
---
### 3.2 User Role Permissions
**listings:**
```json
{
"create": true,
"read": {
"_or": [
{ "status": { "_eq": "published" } },
{ "user_created": { "_eq": "$CURRENT_USER" } }
]
},
"update": {
"user_created": { "_eq": "$CURRENT_USER" }
},
"delete": {
"_and": [
{ "user_created": { "_eq": "$CURRENT_USER" } },
{ "status": { "_in": ["draft", "expired"] } }
]
}
}
```
**conversations:**
```json
{
"create": true,
"read": {
"_or": [
{ "buyer": { "_eq": "$CURRENT_USER" } },
{ "seller": { "_eq": "$CURRENT_USER" } }
]
},
"update": {
"_or": [
{ "buyer": { "_eq": "$CURRENT_USER" } },
{ "seller": { "_eq": "$CURRENT_USER" } }
]
}
}
```
**messages:**
```json
{
"create": {
"conversation": {
"_or": [
{ "buyer": { "_eq": "$CURRENT_USER" } },
{ "seller": { "_eq": "$CURRENT_USER" } }
]
}
},
"read": {
"conversation": {
"_or": [
{ "buyer": { "_eq": "$CURRENT_USER" } },
{ "seller": { "_eq": "$CURRENT_USER" } }
]
}
}
}
```
**favorites:**
```json
{
"create": true,
"read": {
"user": { "_eq": "$CURRENT_USER" }
},
"delete": {
"user": { "_eq": "$CURRENT_USER" }
}
}
```
**reports:**
```json
{
"create": true
}
```
**directus_files:**
```json
{
"create": true,
"read": true,
"update": {
"uploaded_by": { "_eq": "$CURRENT_USER" }
},
"delete": {
"uploaded_by": { "_eq": "$CURRENT_USER" }
}
}
```
**directus_users (eigenes Profil):**
```json
{
"read": {
"id": { "_eq": "$CURRENT_USER" }
},
"update": {
"id": { "_eq": "$CURRENT_USER" }
},
"fields": ["id", "email", "first_name", "last_name", "avatar", "status"]
}
```
---
### 3.3 Moderator Role Permissions
Zusätzlich zu User-Permissions:
**listings:**
```json
{
"read": true,
"update": {
"fields": ["status", "admin_notes"]
}
}
```
**reports:**
```json
{
"read": true,
"update": {
"fields": ["status", "admin_notes", "resolved_by", "resolved_at"]
}
}
```
**directus_users (öffentliche Felder anderer User):**
```json
{
"read": {
"fields": ["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
**Trigger:** `items.create` auf `listings`
```javascript
module.exports = async function(data) {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 30);
return { expires_at: expiresAt.toISOString() };
}
```
---
### 4.3 Set Seller on Conversation Create
**Trigger:** `items.create` auf `conversations`
```javascript
module.exports = async function(data, { database }) {
const listing = await database('listings')
.where('id', data.listing)
.first();
return { seller: listing.user_created };
}
```
---
### 4.4 Increment View Counter
**Trigger:** Custom Endpoint oder Hook
```javascript
// Webhook/Endpoint für View-Tracking
module.exports = async function(data, { database }) {
await database('listings')
.where('id', data.listing_id)
.increment('views', 1);
}
```
---
### 4.5 Auto-Expire Listings (Scheduled)
**Trigger:** Schedule (täglich um 00:00)
```javascript
module.exports = async function(data, { database }) {
const now = new Date().toISOString();
await database('listings')
.where('expires_at', '<', now)
.where('status', 'published')
.update({ status: 'expired' });
}
```
---
## 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
**Environment Variables (.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
```env
# Token Expiry
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=7d
# Password Policy
AUTH_PASSWORD_POLICY=/^.{8,}$/
# Disable Public Registration (optional)
# USER_REGISTER_URL_ALLOW_LIST=https://dgray.io
```
### 5.4 File Storage
```env
STORAGE_LOCATIONS=local
STORAGE_LOCAL_ROOT=./uploads
# Für S3/Cloudflare R2:
# STORAGE_LOCATIONS=s3
# STORAGE_S3_DRIVER=s3
# STORAGE_S3_KEY=xxx
# STORAGE_S3_SECRET=xxx
# STORAGE_S3_BUCKET=dgray-files
# STORAGE_S3_REGION=auto
# STORAGE_S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com
```
### 5.5 Rate Limiting
```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 (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 |
---
## 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)