Files
kashilo/docs/DIRECTUS-SETUP.md

26 KiB
Raw Blame History

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)
  2. User Roles
  3. Access Policies
  4. Flows (Automatisierungen)
  5. Einstellungen
  6. Frontend-Integration
  7. Initiale Daten
  8. Checkliste
  9. Anonyme Authentifizierung (UUID-basiert)
  10. 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

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:

{
  "read": {
    "_and": [
      { "status": { "_eq": "published" } }
    ]
  },
  "fields": ["id", "title", "slug", "description", "price", "currency", "price_type", "category", "condition", "images", "location", "shipping", "date_created", "views"]
}

categories:

{
  "read": {
    "status": { "_eq": "published" }
  },
  "fields": "*"
}

locations:

{
  "read": true,
  "fields": "*"
}

directus_files:

{
  "read": true
}

3.2 User Role Permissions

listings:

{
  "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:

{
  "create": true,
  "read": {
    "_or": [
      { "buyer": { "_eq": "$CURRENT_USER" } },
      { "seller": { "_eq": "$CURRENT_USER" } }
    ]
  },
  "update": {
    "_or": [
      { "buyer": { "_eq": "$CURRENT_USER" } },
      { "seller": { "_eq": "$CURRENT_USER" } }
    ]
  }
}

messages:

{
  "create": {
    "conversation": {
      "_or": [
        { "buyer": { "_eq": "$CURRENT_USER" } },
        { "seller": { "_eq": "$CURRENT_USER" } }
      ]
    }
  },
  "read": {
    "conversation": {
      "_or": [
        { "buyer": { "_eq": "$CURRENT_USER" } },
        { "seller": { "_eq": "$CURRENT_USER" } }
      ]
    }
  }
}

favorites:

{
  "create": true,
  "read": {
    "user": { "_eq": "$CURRENT_USER" }
  },
  "delete": {
    "user": { "_eq": "$CURRENT_USER" }
  }
}

reports:

{
  "create": true
}

directus_files:

{
  "create": true,
  "read": true,
  "update": {
    "uploaded_by": { "_eq": "$CURRENT_USER" }
  },
  "delete": {
    "uploaded_by": { "_eq": "$CURRENT_USER" }
  }
}

directus_users (eigenes Profil):

{
  "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:

{
  "read": true,
  "update": {
    "fields": ["status", "admin_notes"]
  }
}

reports:

{
  "read": true,
  "update": {
    "fields": ["status", "admin_notes", "resolved_by", "resolved_at"]
  }
}

directus_users (öffentliche Felder anderer User):

{
  "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)
// 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

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

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

// 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)

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):

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

# 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

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

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:

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:

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

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.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

// 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

# 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)