20 KiB
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
- 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{ CONVERSATIONS : participates
USERS ||--o{ MESSAGES : sends
USERS ||--o{ REPORTS : submits
LISTINGS ||--o{ LISTINGS_FILES : has
LISTINGS ||--o{ FAVORITES : receives
LISTINGS ||--o{ CONVERSATIONS : has
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 FK
uuid buyer FK
uuid seller FK
string status
}
MESSAGES {
uuid id PK
uuid conversation FK
uuid sender FK
text content
datetime read_at
}
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
}
1. Data Models (Collections)
1.1 listings (Anzeigen)
Die Haupt-Collection für alle Kleinanzeigen.
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key, auto-generated |
status |
String (Dropdown) | draft, published, sold, expired, deleted |
sort |
Integer | Für manuelle Sortierung |
user_created |
User (M2O) | Auto, Read-only |
date_created |
DateTime | Auto, Read-only |
user_updated |
User (M2O) | Auto |
date_updated |
DateTime | Auto |
title |
String | Required, max 100 chars |
slug |
String | Unique, auto-generated from title |
description |
Text (WYSIWYG) | Required |
price |
Decimal | Required, precision 10, scale 2 |
currency |
String | Default: XMR, Options: XMR, EUR |
price_type |
String | fixed, negotiable, free, on_request |
category |
Categories (M2O) | Required |
condition |
String | new, like_new, good, fair, poor |
images |
Files (M2M) | Junction: listings_files |
location |
Locations (M2O) | Optional |
shipping |
Boolean | Versand möglich? |
shipping_cost |
Decimal | Optional |
views |
Integer | Default: 0 |
expires_at |
DateTime | Auto-set, 30 Tage nach Erstellung |
monero_address |
String | Für Direktzahlung |
contact_method |
String | chat, email, both |
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 | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
status |
String | draft, published |
sort |
Integer | Für Sortierung |
name |
String | Required (Fallback-Name) |
slug |
String | Unique |
icon |
String | Icon-Name (z.B. laptop, car) |
parent |
Categories (M2O) | Self-referencing |
translations |
Translations | Junction: categories_translations |
Translations Fields:
| Feld | Typ |
|---|---|
languages_code |
String (de, en, fr) |
name |
String |
description |
Text |
1.3 conversations (Konversationen)
Chat zwischen Käufer und Verkäufer.
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
date_created |
DateTime | Auto |
date_updated |
DateTime | Auto |
listing |
Listings (M2O) | Required |
buyer |
User (M2O) | Wer die Konversation gestartet hat |
seller |
User (M2O) | Auto from listing.user_created |
status |
String | active, archived, blocked |
messages |
Messages (O2M) |
1.4 messages (Nachrichten)
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
date_created |
DateTime | Auto |
conversation |
Conversations (M2O) | Required |
sender |
User (M2O) | Auto (user_created) |
content |
Text | Required, max 2000 chars |
read_at |
DateTime | Null = ungelesen |
type |
String | text, offer, system |
1.5 favorites (Favoriten/Merkliste)
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
date_created |
DateTime | Auto |
user |
User (M2O) | Auto (user_created) |
listing |
Listings (M2O) | Required |
Unique Constraint: user + listing
1.6 reports (Meldungen)
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
date_created |
DateTime | Auto |
reporter |
User (M2O) | Auto |
listing |
Listings (M2O) | Optional |
reported_user |
User (M2O) | Optional |
reason |
String | spam, fraud, inappropriate, illegal, other |
details |
Text | |
status |
String | pending, reviewed, resolved, dismissed |
admin_notes |
Text | Nur für Admins sichtbar |
resolved_by |
User (M2O) | |
resolved_at |
DateTime |
1.7 locations (Orte)
| Feld | Typ | Einstellungen |
|---|---|---|
id |
UUID | Primary Key |
name |
String | Stadt/Ort |
postal_code |
String | PLZ |
region |
String | Bundesland/Kanton |
country |
String | Default: DE |
latitude |
Float | Optional, für Kartenansicht |
longitude |
Float | 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
// Operation: 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, '');
return { slug: `${slug}-${Date.now().toString(36)}` };
}
Operation: Update Data → listings mit Ergebnis
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' });
}
4.6 Welcome Email bei Registrierung
Trigger: users.create
Operation: Send Email
To: {{$trigger.email}}
Subject: Willkommen bei dgray.io
Template: welcome-email
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
5.6 Email Settings
EMAIL_FROM=noreply@dgray.io
EMAIL_TRANSPORT=smtp
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=xxx
EMAIL_SMTP_PASSWORD=xxx
EMAIL_SMTP_SECURE=false
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 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
-
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. Sicherheitshinweise
- Monero-Adressen werden nur dem Listing-Ersteller und authentifizierten Nutzern angezeigt
- Keine echten E-Mails - UUID-basierte Authentifizierung
- Rate Limiting für 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