1011 lines
26 KiB
Markdown
1011 lines
26 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 |
|
||
|
||
**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)
|