28 KiB
Directus Setup für kashilo.com
Komplette Anleitung zur Einrichtung von Directus als Backend für die kashilo Kleinanzeigen-PWA.
API URL: https://api.kashilo.com/
Inhaltsverzeichnis
- Data Models (Collections)
- User Roles
- Access Policies
- Flows (Automatisierungen)
- Einstellungen
- Frontend-Integration
- Initiale Daten
- Checkliste
- Anonyme Authentifizierung (UUID-basiert)
- Sicherheitshinweise
Datenmodell-Übersicht
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:
- Lädt alle Conversations für ein Listing (
listing_idFilter) - Prüft client-seitig, ob eigener Hash in
participant_hash_1oderparticipant_hash_2ist - 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:
- Trigger:
items.createauflistings - Operation 1: Run Script (Slug generieren)
- Operation 2: Update Data (Slug ins Feld schreiben)
// 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:
- Trigger:
items.createauflistings - Operation 1: Run Script (Datum berechnen)
- Operation 2: Update Data (Datum speichern)
// 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:
- Trigger: Webhook (vom Frontend aufgerufen)
- Operation 1: Run Script (Counter erhöhen)
// Trigger: Webhook
Name: increment-views
Method: POST
URL: /flows/trigger/increment-views
// 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:
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:
- Trigger: Schedule (Cron)
- Operation 1: Run Script (Abgelaufene updaten)
// Trigger: Schedule
Type: Schedule (Cron)
Cron: 0 0 * * * (täglich um 00:00 UTC)
// 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: kashilo.com
Project URL: https://kashilo.com
Project Color: #555555
5.2 CORS Settings
Option 1: docker-compose.yml
services:
directus:
image: directus/directus:latest
environment:
CORS_ENABLED: "true"
CORS_ORIGIN: "https://kashilo.com,https://www.kashilo.com,http://localhost:8080"
CORS_METHODS: "GET,POST,PATCH,DELETE"
CORS_ALLOWED_HEADERS: "Content-Type,Authorization"
CORS_CREDENTIALS: "true"
Option 2: .env Datei + Docker Compose
# docker-compose.yml
services:
directus:
env_file: .env
# .env
CORS_ENABLED=true
CORS_ORIGIN=https://kashilo.com,https://www.kashilo.com,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:
# 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:
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)
# docker-compose.yml
environment:
STORAGE_LOCATIONS: "local"
STORAGE_LOCAL_ROOT: "./uploads"
volumes:
- ./uploads:/directus/uploads
Option 2: Hetzner Object Storage (empfohlen)
# 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: "kashilo-files"
STORAGE_S3_REGION: "fsn1" # oder nbg1
STORAGE_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" # oder nbg1
Option 3: Cloudflare R2
# 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: "kashilo-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:
# 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:
RATE_LIMITER_ENABLED=true
RATE_LIMITER_STORE=memory
RATE_LIMITER_POINTS=100
RATE_LIMITER_DURATION=60
Hinweis: Keine E-Mail-Konfiguration nötig - kashilo.com 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) undxmr(XMR-fix)
Nutzung:
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
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
// 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
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)
[
{ "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
[
{ "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.jsService verbinden
9. Anonyme Authentifizierung (UUID-basiert)
Für maximale Privatsphäre nutzt kashilo.com 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@kashilo.com │
│ • 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
// Account erstellen
async function createAnonymousAccount() {
const uuid = crypto.randomUUID();
const email = `${uuid}@kashilo.com`;
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}@kashilo.com`;
await directus.login(email, uuid);
}
9.5 UX-Flow
-
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!"
-
Login:
- User gibt UUID ein (ein Feld)
- System baut E-Mail + Passwort daraus
- Fertig
-
Recovery:
- Nicht möglich (by design)
- Optional: QR-Code zum Offline-Speichern anbieten
9.6 Directus-Konfiguration
# 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
- Monero-Adressen werden nur authentifizierten Nutzern angezeigt
- Rate Limiting für alle API-Endpunkte aktivieren
- Bilder-Upload auf max. 10MB und erlaubte Typen beschränken
- XSS-Schutz für WYSIWYG-Felder in Directus aktivieren
- UUID-Warnung bei Registrierung: User muss UUID sicher speichern
- Kein Passwort-Reset möglich (by design)
- 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)