1012 lines
27 KiB
Markdown
1012 lines
27 KiB
Markdown
# 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)
|