add currency service, update directus setup
This commit is contained in:
@@ -233,7 +233,7 @@ E2E-verschlüsselter Chat mit **Zero-Knowledge Metadaten** - Server weiß nicht,
|
||||
| `user` | User (M2O) | User | Auto (user_created) |
|
||||
| `listing` | Listings (M2O) | Many-to-One | Required |
|
||||
|
||||
**Unique Constraint:** `user` + `listing`
|
||||
**Duplikat-Prüfung:** Frontend-seitig vor dem Erstellen prüfen, ob bereits favorisiert.
|
||||
|
||||
---
|
||||
|
||||
@@ -243,9 +243,9 @@ E2E-verschlüsselter Chat mit **Zero-Knowledge Metadaten** - Server weiß nicht,
|
||||
|------|-----|-----------|---------------|
|
||||
| `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 |
|
||||
| `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` |
|
||||
@@ -328,193 +328,149 @@ App Access: ON
|
||||
|
||||
## 3. Access Policies
|
||||
|
||||
Konfiguration unter: **Settings > Access Control > [Rolle] > [Collection]**
|
||||
|
||||
---
|
||||
|
||||
### 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"]
|
||||
}
|
||||
```
|
||||
Pfad: Settings > Access Control > Public
|
||||
|
||||
**categories:**
|
||||
```json
|
||||
{
|
||||
"read": {
|
||||
"status": { "_eq": "published" }
|
||||
},
|
||||
"fields": "*"
|
||||
}
|
||||
```
|
||||
#### listings
|
||||
|
||||
**locations:**
|
||||
```json
|
||||
{
|
||||
"read": true,
|
||||
"fields": "*"
|
||||
}
|
||||
```
|
||||
| Aktion | Erlaubt | Filter |
|
||||
|--------|---------|--------|
|
||||
| Read | ✅ Custom | `status` equals `published` |
|
||||
| Create | ❌ | – |
|
||||
| Update | ❌ | – |
|
||||
| Delete | ❌ | – |
|
||||
|
||||
**directus_files:**
|
||||
```json
|
||||
{
|
||||
"read": true
|
||||
}
|
||||
```
|
||||
**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
|
||||
|
||||
**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"] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
Pfad: Settings > Access Control > User
|
||||
|
||||
**conversations:**
|
||||
```json
|
||||
{
|
||||
"create": true,
|
||||
"read": {
|
||||
"_or": [
|
||||
{ "buyer": { "_eq": "$CURRENT_USER" } },
|
||||
{ "seller": { "_eq": "$CURRENT_USER" } }
|
||||
]
|
||||
},
|
||||
"update": {
|
||||
"_or": [
|
||||
{ "buyer": { "_eq": "$CURRENT_USER" } },
|
||||
{ "seller": { "_eq": "$CURRENT_USER" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
#### listings
|
||||
|
||||
**messages:**
|
||||
```json
|
||||
{
|
||||
"create": {
|
||||
"conversation": {
|
||||
"_or": [
|
||||
{ "buyer": { "_eq": "$CURRENT_USER" } },
|
||||
{ "seller": { "_eq": "$CURRENT_USER" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"read": {
|
||||
"conversation": {
|
||||
"_or": [
|
||||
{ "buyer": { "_eq": "$CURRENT_USER" } },
|
||||
{ "seller": { "_eq": "$CURRENT_USER" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
| 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` |
|
||||
|
||||
**favorites:**
|
||||
```json
|
||||
{
|
||||
"create": true,
|
||||
"read": {
|
||||
"user": { "_eq": "$CURRENT_USER" }
|
||||
},
|
||||
"delete": {
|
||||
"user": { "_eq": "$CURRENT_USER" }
|
||||
}
|
||||
}
|
||||
```
|
||||
#### conversations
|
||||
|
||||
**reports:**
|
||||
```json
|
||||
{
|
||||
"create": true
|
||||
}
|
||||
```
|
||||
| Aktion | Erlaubt | Filter |
|
||||
|--------|---------|--------|
|
||||
| Create | ✅ All | – |
|
||||
| Read | ✅ All | – |
|
||||
| Update | ✅ All | – |
|
||||
|
||||
**directus_files:**
|
||||
```json
|
||||
{
|
||||
"create": true,
|
||||
"read": true,
|
||||
"update": {
|
||||
"uploaded_by": { "_eq": "$CURRENT_USER" }
|
||||
},
|
||||
"delete": {
|
||||
"uploaded_by": { "_eq": "$CURRENT_USER" }
|
||||
}
|
||||
}
|
||||
```
|
||||
**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
|
||||
|
||||
**directus_users (eigenes Profil):**
|
||||
```json
|
||||
{
|
||||
"read": {
|
||||
"id": { "_eq": "$CURRENT_USER" }
|
||||
},
|
||||
"update": {
|
||||
"id": { "_eq": "$CURRENT_USER" }
|
||||
},
|
||||
"fields": ["id", "email", "first_name", "last_name", "avatar", "status"]
|
||||
}
|
||||
```
|
||||
#### 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
|
||||
|
||||
Zusätzlich zu User-Permissions:
|
||||
Pfad: Settings > Access Control > Moderator
|
||||
|
||||
**listings:**
|
||||
```json
|
||||
{
|
||||
"read": true,
|
||||
"update": {
|
||||
"fields": ["status", "admin_notes"]
|
||||
}
|
||||
}
|
||||
```
|
||||
Alle User-Permissions plus:
|
||||
|
||||
**reports:**
|
||||
```json
|
||||
{
|
||||
"read": true,
|
||||
"update": {
|
||||
"fields": ["status", "admin_notes", "resolved_by", "resolved_at"]
|
||||
}
|
||||
}
|
||||
```
|
||||
#### listings
|
||||
|
||||
**directus_users (öffentliche Felder anderer User):**
|
||||
```json
|
||||
{
|
||||
"read": {
|
||||
"fields": ["id", "first_name", "avatar", "status"]
|
||||
}
|
||||
}
|
||||
```
|
||||
| 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`
|
||||
|
||||
---
|
||||
|
||||
@@ -558,9 +514,16 @@ Payload: { "slug": "{{$last.slug}}" }
|
||||
|
||||
### 4.2 Auto-Set Expiry Date
|
||||
|
||||
**Trigger:** `items.create` auf `listings`
|
||||
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);
|
||||
@@ -568,54 +531,88 @@ module.exports = async function(data) {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 };
|
||||
}
|
||||
```
|
||||
// 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.4 Increment View Counter
|
||||
### 4.3 Increment View Counter
|
||||
|
||||
**Trigger:** Custom Endpoint oder Hook
|
||||
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
|
||||
// Webhook/Endpoint für View-Tracking
|
||||
// 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.5 Auto-Expire Listings (Scheduled)
|
||||
### 4.4 Auto-Expire Listings (Scheduled)
|
||||
|
||||
**Trigger:** Schedule (täglich um 00:00)
|
||||
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();
|
||||
|
||||
await database('listings')
|
||||
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
|
||||
@@ -632,9 +629,31 @@ Project Color: #555555
|
||||
|
||||
### 5.2 CORS Settings
|
||||
|
||||
**Environment Variables (.env):**
|
||||
**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
|
||||
@@ -644,36 +663,68 @@ 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
|
||||
# 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
|
||||
AUTH_PASSWORD_POLICY=/^.{36,}$/
|
||||
PUBLIC_REGISTRATION=true
|
||||
```
|
||||
|
||||
### 5.4 File Storage
|
||||
|
||||
```env
|
||||
STORAGE_LOCATIONS=local
|
||||
STORAGE_LOCAL_ROOT=./uploads
|
||||
Konfiguration via **docker-compose.yml** oder **.env Datei**:
|
||||
|
||||
# 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
|
||||
**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
|
||||
@@ -683,92 +734,42 @@ 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)
|
||||
### 5.6 Währungsumrechnung & Preismodus
|
||||
|
||||
Für die Anzeige von Fiat-Preisen in XMR wird die Kraken API genutzt.
|
||||
Implementiert im Frontend-Service: **`js/services/currency.js`**
|
||||
|
||||
**API Endpoint:**
|
||||
```
|
||||
https://api.kraken.com/0/public/Ticker?pair=XMRUSD,XMREUR,XMRGBP,XMRCHF,XMRJPY
|
||||
```
|
||||
**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)
|
||||
|
||||
**Frontend-Implementation:**
|
||||
**Nutzung:**
|
||||
```javascript
|
||||
const KRAKEN_API = 'https://api.kraken.com/0/public/Ticker';
|
||||
const PAIRS = {
|
||||
USD: 'XMRUSD',
|
||||
EUR: 'XMREUR',
|
||||
GBP: 'XMRGBP',
|
||||
CHF: 'XMRCHF',
|
||||
JPY: 'XMRJPY'
|
||||
};
|
||||
import { getXmrRates, formatPrice } from './services/currency.js';
|
||||
|
||||
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;
|
||||
}
|
||||
// Kurse laden
|
||||
const rates = await getXmrRates();
|
||||
|
||||
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);
|
||||
// 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 }
|
||||
```
|
||||
|
||||
**Caching:** Rates werden client-side für 5 Minuten gecached.
|
||||
|
||||
### 5.7 Preismodus (Fiat vs. XMR)
|
||||
|
||||
Listings können in zwei Modi erstellt werden:
|
||||
**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 |
|
||||
|
||||
**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 (bei 1 XMR = 150 EUR):**
|
||||
|
||||
**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 |
|
||||
| Listing | Anzeige |
|
||||
|---------|---------|
|
||||
| 100 EUR, mode=`fiat` | **€ 100,00** ≈ 0.6667 XMR |
|
||||
| 0.5 XMR, mode=`xmr` | **0.5000 XMR** ≈ € 75,00 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user