remove all Cloudflare Workers references and cleanup
Removed: - workers/ directory (Cloudflare Worker scripts) - public/workers/ (Worker scripts in public) - Cloudflare setup documentation - workerUrl from Database and ConfigService interfaces - Outdated documentation files (ARCHITECTURE, IMPLEMENTATION-SUMMARY, QUICK-START) - IOS-WEB-STRATEGY.md Updated: - README.md - now focuses on Uberspace deployment only - ConfigService.getApiUrl() - removed workerUrl parameter - SettingsDetailPage - removed config.workerUrl usage - Database.DbConfig - removed workerUrl and cloudflare fields The app now uses only Uberspace for both frontend and backend. No more multi-deployment complexity. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
131
ARCHITECTURE.md
131
ARCHITECTURE.md
@@ -1,131 +0,0 @@
|
|||||||
# WhatToPlay - Architektur Entscheidung
|
|
||||||
|
|
||||||
## Problem: Gaming Platform APIs für iOS/Web
|
|
||||||
|
|
||||||
### Services Status:
|
|
||||||
|
|
||||||
- ✅ **Steam**: Öffentliche Web API (`GetOwnedGames`) - funktioniert im Browser/iOS
|
|
||||||
- ⚠️ **GOG**: Galaxy Library API - benötigt OAuth (Server-Side Token Exchange)
|
|
||||||
- ❌ **Epic Games**: Keine öffentliche API - nur über Legendary CLI (Python)
|
|
||||||
- ❌ **Amazon Games**: Keine öffentliche API - nur über Nile CLI (Python)
|
|
||||||
|
|
||||||
### Warum CLI-Tools nicht funktionieren:
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ Python/Node CLI Tools (Legendary, Nile, gogdl)
|
|
||||||
└─> Benötigen native Runtime
|
|
||||||
└─> Funktioniert NICHT auf iOS
|
|
||||||
└─> Funktioniert NICHT im Browser
|
|
||||||
└─> Funktioniert NICHT als reine Web-App
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lösung: Hybrid-Architektur
|
|
||||||
|
|
||||||
### Phase 1: MVP (Jetzt)
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend (React/Ionic)
|
|
||||||
↓
|
|
||||||
Steam Web API (direkt)
|
|
||||||
- GetOwnedGames Endpoint
|
|
||||||
- Keine Auth nötig (nur API Key)
|
|
||||||
- Funktioniert im Browser
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: GOG Integration (wenn Backend da ist)
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend (React/Ionic)
|
|
||||||
↓
|
|
||||||
Backend (Vercel Function / Cloudflare Worker)
|
|
||||||
↓
|
|
||||||
GOG Galaxy API
|
|
||||||
- OAuth Token Exchange (Server-Side)
|
|
||||||
- Library API mit Bearer Token
|
|
||||||
- CORS-Safe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Epic/Amazon (Zukunft)
|
|
||||||
|
|
||||||
**Option A: Backend Proxy**
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend → Backend → Epic GraphQL (Reverse-Engineered)
|
|
||||||
→ Amazon Nile API
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B: Manuelle Import-Funktion**
|
|
||||||
|
|
||||||
```
|
|
||||||
User exportiert Library aus Epic/Amazon
|
|
||||||
↓
|
|
||||||
User uploaded JSON in App
|
|
||||||
↓
|
|
||||||
App parsed und zeigt an
|
|
||||||
```
|
|
||||||
|
|
||||||
## Aktuelle Implementation
|
|
||||||
|
|
||||||
### Steam (✅ Funktioniert jetzt)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// fetch-steam.mjs
|
|
||||||
const response = await fetch(
|
|
||||||
`http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/`,
|
|
||||||
{ params: { key, steamid, format: "json" } },
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### GOG (⚠️ Vorbereitet, braucht Backend)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Jetzt: Manueller Token aus Browser DevTools
|
|
||||||
// Später: OAuth Flow über Backend
|
|
||||||
const response = await fetch(
|
|
||||||
`https://galaxy-library.gog.com/users/${userId}/releases`,
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Epic/Amazon (❌ Placeholder)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Aktuell: Leere JSON-Dateien als Platzhalter
|
|
||||||
// Später: Backend-Integration oder manuelle Import-Funktion
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Strategie
|
|
||||||
|
|
||||||
### Development (macOS - Jetzt)
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run fetch → Lokale Node.js Scripts holen Daten
|
|
||||||
npm run dev → Vite Dev Server mit Hot Reload
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production (iOS/Web - Später)
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend: Vercel/Netlify (Static React App)
|
|
||||||
Backend: Vercel Functions (für GOG OAuth)
|
|
||||||
Data: Supabase/Firebase (für User Libraries)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nächste Schritte
|
|
||||||
|
|
||||||
1. ✅ **Steam**: Fertig implementiert
|
|
||||||
2. 🔄 **GOG**: Manuelle Token-Eingabe (Development)
|
|
||||||
3. 📝 **Epic/Amazon**: Placeholder JSON
|
|
||||||
4. 🚀 **Backend**: OAuth-Service für GOG (Vercel Function)
|
|
||||||
5. 📱 **iOS**: PWA mit Service Worker für Offline-Support
|
|
||||||
|
|
||||||
## Wichtige Limitierungen
|
|
||||||
|
|
||||||
- **Keine nativen CLI-Tools** in Production
|
|
||||||
- **CORS** blockiert direkte Browser → Gaming APIs
|
|
||||||
- **OAuth Secrets** können nicht im Browser gespeichert werden
|
|
||||||
- **Backend ist Pflicht** für GOG/Epic/Amazon
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Fazit**: Für iOS/Web müssen wir ein Backend bauen. Steam funktioniert ohne Backend, GOG/Epic/Amazon brauchen Server-Side OAuth.
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
# IMPLEMENTATION SUMMARY - Februar 2026
|
|
||||||
|
|
||||||
## ✅ Was wurde implementiert
|
|
||||||
|
|
||||||
### 1. Settings-Tab mit vollständiger Konfiguration
|
|
||||||
|
|
||||||
- **UI Component**: `src/pages/Settings/SettingsPage.tsx`
|
|
||||||
- **Styling**: `src/pages/Settings/SettingsPage.css`
|
|
||||||
- **Features**:
|
|
||||||
- ✅ Separate Karten für jeden Gaming-Service
|
|
||||||
- ✅ Input-Felder für API Keys, IDs, Tokens (sicher - mit `type="password"`)
|
|
||||||
- ✅ Dropdown-Selektoren (z.B. Blizzard Region)
|
|
||||||
- ✅ Config Export/Import (JSON Download/Upload)
|
|
||||||
- ✅ "Alle Einstellungen löschen" Button
|
|
||||||
- ✅ Responsive Design für iOS/Web
|
|
||||||
|
|
||||||
### 2. Integriertes Tutorial-System
|
|
||||||
|
|
||||||
- **Component**: `src/components/TutorialModal.tsx`
|
|
||||||
- **Coverage**: 5 Services (Steam, GOG, Epic, Amazon, Blizzard)
|
|
||||||
- **Pro Service**: 4-6 Schritte + Tipps
|
|
||||||
- **Features**:
|
|
||||||
- ✅ Step-by-Step Guides mit Code-Beispielen
|
|
||||||
- ✅ Hinweise und Warnung-Boxen
|
|
||||||
- ✅ Links zu offiziellen Dokumentationen
|
|
||||||
- ✅ Modal-Dialog (nicht inline)
|
|
||||||
|
|
||||||
### 3. ConfigService - Sichere Speicherung
|
|
||||||
|
|
||||||
- **Service**: `src/services/ConfigService.ts`
|
|
||||||
- **Storage-Backend**:
|
|
||||||
- ✅ localStorage (schnell, 5-10MB)
|
|
||||||
- ✅ IndexedDB (Backup, 50MB+)
|
|
||||||
- ✅ Export/Import Funktionen
|
|
||||||
- **Validierung**: Prüft auf erforderliche Felder
|
|
||||||
- **Sicherheit**: Keine Verschlüsselung (würde Usability schaden)
|
|
||||||
|
|
||||||
### 4. Blizzard API Integration
|
|
||||||
|
|
||||||
- **Importer**: `scripts/fetch-blizzard.mjs`
|
|
||||||
- **OAuth-Flow**: Client Credentials (Token Exchange)
|
|
||||||
- **Unterstützte Games**:
|
|
||||||
- World of Warcraft
|
|
||||||
- Diablo III (Heroes)
|
|
||||||
- Diablo IV
|
|
||||||
- Overwatch 2
|
|
||||||
- StarCraft II
|
|
||||||
- Heroes of the Storm
|
|
||||||
- Hearthstone
|
|
||||||
- **Data**: Level, Class, Kills, Hardcore Flag, Last Updated
|
|
||||||
|
|
||||||
### 5. Cloudflare Workers Dokumentation
|
|
||||||
|
|
||||||
- **Datei**: `docs/CLOUDFLARE-WORKERS-SETUP.md`
|
|
||||||
- **Coverage**:
|
|
||||||
- ✅ GOG OAuth Worker (Complete)
|
|
||||||
- ✅ Blizzard OAuth Worker (Complete)
|
|
||||||
- ✅ Deployment Instructions
|
|
||||||
- ✅ Security Best Practices
|
|
||||||
- ✅ KV Store Setup
|
|
||||||
- ✅ Debugging Guide
|
|
||||||
|
|
||||||
### 6. App Navigation Update
|
|
||||||
|
|
||||||
- **File**: `src/App.tsx`
|
|
||||||
- **Änderung**: Settings-Tab hinzugefügt (#5 von 5)
|
|
||||||
- **Icon**: `settingsOutline` von ionicons
|
|
||||||
|
|
||||||
### 7. Dokumentation & Guides
|
|
||||||
|
|
||||||
- **QUICK-START.md**: 5-Minuten Einstieg
|
|
||||||
- **BLIZZARD-SETUP.md**: OAuth Konfiguration
|
|
||||||
- **FEATURES-OVERVIEW.md**: Gesamtübersicht
|
|
||||||
- **CLOUDFLARE-WORKERS-SETUP.md**: Backend Deployment
|
|
||||||
- **config.local.json.example**: Config Template
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Code Statistics
|
|
||||||
|
|
||||||
| Komponente | Zeilen | Komplexität |
|
|
||||||
| --------------------------- | ------ | -------------------- |
|
|
||||||
| SettingsPage.tsx | 380 | Mittel |
|
|
||||||
| TutorialModal.tsx | 420 | Mittel |
|
|
||||||
| ConfigService.ts | 140 | Einfach |
|
|
||||||
| fetch-blizzard.mjs | 180 | Mittel |
|
|
||||||
| CLOUDFLARE-WORKERS-SETUP.md | 450 | Hoch (Dokumentation) |
|
|
||||||
|
|
||||||
**Gesamt neue Code**: ~1.570 Zeilen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Architektur-Entscheidungen
|
|
||||||
|
|
||||||
### localStorage + IndexedDB Hybrid
|
|
||||||
|
|
||||||
```
|
|
||||||
Warum?
|
|
||||||
• localStorage: Schnell, einfach, < 5MB
|
|
||||||
• IndexedDB: Großer Storage, Backup-ready
|
|
||||||
• Beide Client-Side = Offline-Ready
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cloudflare Workers statt Vercel Functions
|
|
||||||
|
|
||||||
```
|
|
||||||
Warum?
|
|
||||||
• Zero Configuration (vs. Vercel config)
|
|
||||||
• KV Store integriert (vs. external DB)
|
|
||||||
• Better Edge Performance (distributed)
|
|
||||||
• Free tier ist großzügig
|
|
||||||
• Secrets natürlich geschützt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Client Credentials Flow (nicht Authorization Code)
|
|
||||||
|
|
||||||
```
|
|
||||||
Warum?
|
|
||||||
• Blizzard erlaubt nur Client Credentials
|
|
||||||
• Keine User Consent nötig
|
|
||||||
• Einfacher OAuth Flow
|
|
||||||
• Secretmanagement einfacher
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 Sicherheit
|
|
||||||
|
|
||||||
### ✅ Implementiert
|
|
||||||
|
|
||||||
- Client Secrets in Backend nur (Cloudflare KV Store)
|
|
||||||
- Token Export/Import mit Warnung
|
|
||||||
- Password Input Fields (verborgen)
|
|
||||||
- CORS auf Cloudflare Worker konfigurierbar
|
|
||||||
- State Parameter für CSRF (in Worker)
|
|
||||||
|
|
||||||
### ⚠️ Bewusst NICHT implementiert
|
|
||||||
|
|
||||||
- Token Verschlüsselung in localStorage (UX Impact)
|
|
||||||
- 2FA für Settings (Overkill für MVP)
|
|
||||||
- Audit Logs (später, wenn selbst-gehostet)
|
|
||||||
- Rate Limiting (kommt auf Server-Side)
|
|
||||||
|
|
||||||
**Reasoning**: MVP-Fokus auf Usability, nicht auf Enterprise-Security
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
| Metrik | Wert | Note |
|
|
||||||
| ------------------- | ------ | --------------------- |
|
|
||||||
| Settings Load | <10ms | localStorage nur |
|
|
||||||
| Config Save | <1ms | IndexedDB async |
|
|
||||||
| Tutorial Modal Open | <50ms | React render |
|
|
||||||
| Export (1000 Games) | <200ms | JSON stringify |
|
|
||||||
| Import (1000 Games) | <500ms | JSON parse + validate |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment Readiness
|
|
||||||
|
|
||||||
### Frontend (Vite)
|
|
||||||
|
|
||||||
```
|
|
||||||
Status: ✅ Production-Ready
|
|
||||||
npm run build → dist/
|
|
||||||
Deployment: Vercel, Netlify, GitHub Pages
|
|
||||||
CORS: Handled via Cloudflare Worker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend (Cloudflare Workers)
|
|
||||||
|
|
||||||
```
|
|
||||||
Status: ⚠️ Dokumentiert, nicht deployed
|
|
||||||
Bedarf:
|
|
||||||
1. Cloudflare Account (kostenlos)
|
|
||||||
2. GOG Client ID + Secret
|
|
||||||
3. Blizzard Client ID + Secret
|
|
||||||
4. npx wrangler deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Storage
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend: localStorage + IndexedDB
|
|
||||||
Backend: Cloudflare KV Store (für Secrets)
|
|
||||||
Optional: Supabase für Cloud-Sync
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Noch zu tun für Production
|
|
||||||
|
|
||||||
### Sofort (< 1 Woche)
|
|
||||||
|
|
||||||
- [ ] Cloudflare Worker deployen
|
|
||||||
- [ ] GOG/Blizzard Credentials besorgen
|
|
||||||
- [ ] KV Store konfigurieren
|
|
||||||
- [ ] CORS testen
|
|
||||||
|
|
||||||
### Bald (1-2 Wochen)
|
|
||||||
|
|
||||||
- [ ] Epic Games JSON Import UI
|
|
||||||
- [ ] Amazon Games JSON Import UI
|
|
||||||
- [ ] Token Refresh Logic
|
|
||||||
- [ ] Error Boundary Components
|
|
||||||
|
|
||||||
### Later (2-4 Wochen)
|
|
||||||
|
|
||||||
- [ ] Home-Page Widgets
|
|
||||||
- [ ] Playlists Feature
|
|
||||||
- [ ] Discover/Tinder UI
|
|
||||||
- [ ] PWA Service Worker
|
|
||||||
|
|
||||||
### Optional (4+ Wochen)
|
|
||||||
|
|
||||||
- [ ] Cloud-Sync (Supabase)
|
|
||||||
- [ ] Native iOS App (React Native)
|
|
||||||
- [ ] Social Features (Friends)
|
|
||||||
- [ ] Recommendations Engine
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Lernpunkte
|
|
||||||
|
|
||||||
### OAuth Flows
|
|
||||||
|
|
||||||
- ✅ Client Credentials (Blizzard)
|
|
||||||
- ⚠️ Authorization Code (GOG, dokumentiert)
|
|
||||||
- ❌ PKCE (zukünftig für Web)
|
|
||||||
|
|
||||||
### Storage Patterns
|
|
||||||
|
|
||||||
- ✅ Single Source of Truth (ConfigService)
|
|
||||||
- ✅ Backup + Restore (IndexedDB)
|
|
||||||
- ✅ Export/Import (JSON)
|
|
||||||
|
|
||||||
### Component Design
|
|
||||||
|
|
||||||
- ✅ Data-Driven Tutorials (TUTORIALS Objekt)
|
|
||||||
- ✅ Observable Pattern (setState + Service)
|
|
||||||
- ✅ Modal System (TutorialModal)
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
|
|
||||||
- ✅ Serverless (Cloudflare)
|
|
||||||
- ✅ No Database (localStorage MVP)
|
|
||||||
- ✅ Secret Management (KV Store)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Referenzen
|
|
||||||
|
|
||||||
### Services & APIs
|
|
||||||
|
|
||||||
- [Steam Web API](https://developer.valvesoftware.com/wiki/Steam_Web_API)
|
|
||||||
- [GOG Galaxy API](https://galaxy-library.gog.com/)
|
|
||||||
- [Blizzard OAuth](https://develop.battle.net/documentation/guides/using-oauth)
|
|
||||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
- React 18.2 + TypeScript
|
|
||||||
- Ionic React (iOS Mode)
|
|
||||||
- Vite 5.0
|
|
||||||
- Cloudflare Workers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Ergebnis
|
|
||||||
|
|
||||||
**Komplette, produktionsreife Konfigurationsseite mit:**
|
|
||||||
|
|
||||||
- ✅ 5 Gaming-Services
|
|
||||||
- ✅ Integriertes Tutorial-System
|
|
||||||
- ✅ Sichere Speicherung
|
|
||||||
- ✅ Export/Import Funktionalität
|
|
||||||
- ✅ Zero Infrastructure Backend (Cloudflare)
|
|
||||||
- ✅ iOS/Web kompatibel
|
|
||||||
- ✅ Offline funktional
|
|
||||||
- ✅ Umfassende Dokumentation
|
|
||||||
|
|
||||||
**Zeitaufwand**: ~2-3 Stunden
|
|
||||||
**Code-Qualität**: Production-Ready
|
|
||||||
**Dokumentation**: Exzellent
|
|
||||||
318
QUICK-START.md
318
QUICK-START.md
@@ -1,318 +0,0 @@
|
|||||||
# WhatToPlay - Quick Start Guide
|
|
||||||
|
|
||||||
## 🚀 Schnelleinstieg (5 Minuten)
|
|
||||||
|
|
||||||
### 1. App öffnen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /Users/felixfoertsch/Developer/whattoplay
|
|
||||||
npm run dev
|
|
||||||
# Opens: http://localhost:5173
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Settings-Tab öffnen
|
|
||||||
|
|
||||||
```
|
|
||||||
Navbar unten rechts → "Einstellungen" Tab
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Steam integrieren (optional, funktioniert sofort)
|
|
||||||
|
|
||||||
```
|
|
||||||
Settings Tab
|
|
||||||
↓
|
|
||||||
Karte "🎮 Steam"
|
|
||||||
↓
|
|
||||||
"?" Button → Tutorial Modal
|
|
||||||
↓
|
|
||||||
Folge den 6 Schritten:
|
|
||||||
1. https://steamcommunity.com/dev/apikey
|
|
||||||
2. Login & Accept ToS
|
|
||||||
3. API Key kopieren
|
|
||||||
4. https://www.steamcommunity.com/
|
|
||||||
5. Auf Namen klicken
|
|
||||||
6. Steam ID aus URL kopieren (z.B. 76561197960434622)
|
|
||||||
↓
|
|
||||||
Eintragen → Speichern
|
|
||||||
↓
|
|
||||||
Library Tab → 1103 Games erscheinen!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎮 Für jeden Service
|
|
||||||
|
|
||||||
### Steam ✅ (Funktioniert JETZT)
|
|
||||||
|
|
||||||
```
|
|
||||||
Difficulty: ⭐ Einfach
|
|
||||||
Time: 5 Minuten
|
|
||||||
Status: Voll funktionsfähig
|
|
||||||
```
|
|
||||||
|
|
||||||
### GOG ⚠️ (Funktioniert JETZT mit manuelem Token)
|
|
||||||
|
|
||||||
```
|
|
||||||
Difficulty: ⭐⭐ Mittel
|
|
||||||
Time: 10 Minuten
|
|
||||||
Status: Development-ready
|
|
||||||
Step: Tutorial → Browser DevTools → Token kopieren
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blizzard ⚠️ (Funktioniert JETZT mit Credentials)
|
|
||||||
|
|
||||||
```
|
|
||||||
Difficulty: ⭐⭐ Mittel
|
|
||||||
Time: 10 Minuten
|
|
||||||
Status: Development-ready
|
|
||||||
Step: Docs → OAuth → Client ID + Secret
|
|
||||||
```
|
|
||||||
|
|
||||||
### Epic Games ⚠️ (Später, mit Backend)
|
|
||||||
|
|
||||||
```
|
|
||||||
Difficulty: ⭐⭐⭐ Schwer
|
|
||||||
Time: 30+ Minuten
|
|
||||||
Status: Needs Cloudflare Worker
|
|
||||||
Step: Warte auf Backend OAuth Proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Amazon Games ⚠️ (Später, mit Backend)
|
|
||||||
|
|
||||||
```
|
|
||||||
Difficulty: ⭐⭐⭐ Schwer
|
|
||||||
Time: 30+ Minuten
|
|
||||||
Status: Needs Cloudflare Worker
|
|
||||||
Step: Warte auf Backend OAuth Proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💾 Config Management
|
|
||||||
|
|
||||||
### Export (Backup machen)
|
|
||||||
|
|
||||||
```
|
|
||||||
Settings Tab
|
|
||||||
↓
|
|
||||||
"📦 Daten-Management"
|
|
||||||
↓
|
|
||||||
"Config exportieren"
|
|
||||||
↓
|
|
||||||
whattoplay-config.json herunterladen
|
|
||||||
↓
|
|
||||||
(WARNUNG: Enthält sensitive Daten! Sicher lagern!)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import (Von anderem Device)
|
|
||||||
|
|
||||||
```
|
|
||||||
Settings Tab
|
|
||||||
↓
|
|
||||||
"📦 Daten-Management"
|
|
||||||
↓
|
|
||||||
"Config importieren"
|
|
||||||
↓
|
|
||||||
whattoplay-config.json auswählen
|
|
||||||
↓
|
|
||||||
✓ Alles wiederhergestellt!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Häufige Probleme
|
|
||||||
|
|
||||||
### "Keine Games angezeigt"
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Settings-Tab überprüfen
|
|
||||||
2. Alle Felder gefüllt? ✓
|
|
||||||
3. Library-Tab laden lassen (30 Sekunden)
|
|
||||||
4. Browser-Konsole öffnen (F12) → Fehler checken
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Steam ID nicht gültig"
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ Richtig: 76561197960434622 (lange Nummer)
|
|
||||||
❌ Falsch: felixfoertsch (Name/Community ID)
|
|
||||||
|
|
||||||
→ Gehe zu https://www.steamcommunity.com/
|
|
||||||
→ Öffne dein Profil
|
|
||||||
→ URL ist: /profiles/76561197960434622/
|
|
||||||
→ Diese Nummer kopieren!
|
|
||||||
```
|
|
||||||
|
|
||||||
### "GOG Token abgelaufen"
|
|
||||||
|
|
||||||
```
|
|
||||||
Tokens laufen nach ~24h ab
|
|
||||||
|
|
||||||
→ Settings Tab
|
|
||||||
→ GOG Karte
|
|
||||||
→ Neuer Token aus Browser (Follow Tutorial)
|
|
||||||
→ Speichern
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Blizzard sagt 'invalid client'"
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Client ID/Secret überprüfen
|
|
||||||
2. Battle.net Developer Portal:
|
|
||||||
https://develop.battle.net
|
|
||||||
3. "My Applications" öffnen
|
|
||||||
4. Correct Credentials kopieren
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📱 Auf dem iPhone nutzen
|
|
||||||
|
|
||||||
### Option 1: Web App (Empfohlen)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. iPhone Safari
|
|
||||||
2. Gehe zu https://whattoplay.vercel.app (später)
|
|
||||||
3. Teilen → Home Screen hinzufügen
|
|
||||||
4. App sieht aus wie native App!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Localhost (Development)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. iPhone und Computer im gleichen WiFi
|
|
||||||
2. Computer IP: 192.168.x.x
|
|
||||||
3. iPhone Safari: 192.168.x.x:5173
|
|
||||||
4. Funktioniert auch ohne Internet (offline!)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Workflow zum Hinzufügen neuer Games
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Spiel auf Steam/GOG/Epic spielen
|
|
||||||
2. Settings speichern (automatisch täglich?)
|
|
||||||
3. Library Tab öffnen
|
|
||||||
4. Neue Spiele erscheinen
|
|
||||||
5. Click auf Spiel → Details
|
|
||||||
6. Zu Playlist hinzufügen (später)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 MVP vs. Production
|
|
||||||
|
|
||||||
### MVP (Jetzt, February 2026)
|
|
||||||
|
|
||||||
- ✅ Steam funktioniert perfekt
|
|
||||||
- ✅ Settings-Tab mit Tutorials
|
|
||||||
- ✅ GOG/Blizzard Development-ready
|
|
||||||
- ⚠️ Epic/Amazon nur placeholder
|
|
||||||
- ✅ Config Export/Import
|
|
||||||
- ✅ Offline funktional (localStorage)
|
|
||||||
|
|
||||||
### Production (März+ 2026)
|
|
||||||
|
|
||||||
- Cloudflare Worker deployen
|
|
||||||
- GOG/Blizzard OAuth automatisch
|
|
||||||
- Epic/Amazon manueller Import
|
|
||||||
- Home-Page Widgets
|
|
||||||
- Playlists Feature
|
|
||||||
- PWA + iOS App
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Dokumentation
|
|
||||||
|
|
||||||
| Datei | Inhalt |
|
|
||||||
| ------------------------------------------------------------ | -------------------- |
|
|
||||||
| [FEATURES-OVERVIEW.md](./FEATURES-OVERVIEW.md) | Was gibt es neues? |
|
|
||||||
| [CLOUDFLARE-WORKERS-SETUP.md](./CLOUDFLARE-WORKERS-SETUP.md) | Backend deployen |
|
|
||||||
| [BLIZZARD-SETUP.md](./BLIZZARD-SETUP.md) | Blizzard OAuth |
|
|
||||||
| [GOG-SETUP.md](./GOG-SETUP.md) | GOG Token extraction |
|
|
||||||
| [IOS-WEB-STRATEGY.md](./IOS-WEB-STRATEGY.md) | Gesamtstrategie |
|
|
||||||
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Technische Details |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Pro Tipps
|
|
||||||
|
|
||||||
### Mehrere Accounts gleichzeitig
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser-Profile nutzen:
|
|
||||||
↓
|
|
||||||
Chrome/Firefox: Neue Person/Profil
|
|
||||||
↓
|
|
||||||
Unterschiedliche config.local.json je Profil
|
|
||||||
↓
|
|
||||||
Vergleiche deine Bibliothek mit Freunden!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Spiele schneller finden
|
|
||||||
|
|
||||||
```
|
|
||||||
Library Tab
|
|
||||||
↓
|
|
||||||
Suchleiste (zukünftig):
|
|
||||||
- Nach Titel suchen
|
|
||||||
- Nach Plattform filtern
|
|
||||||
- Nach Länge sortieren
|
|
||||||
```
|
|
||||||
|
|
||||||
### Offline Modus
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Settings speichern (einmalig online)
|
|
||||||
2. Dann brauchst du kein Internet mehr
|
|
||||||
3. Daten in localStorage gespeichert
|
|
||||||
4. Auf dem Flugzeug spielen? ✓ Funktioniert!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Nächste Schritte für dich
|
|
||||||
|
|
||||||
### Sofort testen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# → Settings Tab → Steam Tutorial folgen
|
|
||||||
```
|
|
||||||
|
|
||||||
### In 1 Woche
|
|
||||||
|
|
||||||
```
|
|
||||||
- GOG oder Blizzard einrichten
|
|
||||||
- Config exportieren
|
|
||||||
- Alle Games konsolidiert sehen
|
|
||||||
```
|
|
||||||
|
|
||||||
### In 2 Wochen
|
|
||||||
|
|
||||||
```
|
|
||||||
- Cloudflare Worker aufsetzen
|
|
||||||
- OAuth automatisieren
|
|
||||||
- Epic/Amazon hinzufügen (einfacher)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ Fragen?
|
|
||||||
|
|
||||||
Siehe `docs/` Ordner für detaillierte Guides:
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/
|
|
||||||
├── FEATURES-OVERVIEW.md (Was gibt es neues?)
|
|
||||||
├── CLOUDFLARE-WORKERS-SETUP.md (Zero-Infra Backend)
|
|
||||||
├── BLIZZARD-SETUP.md (Blizzard OAuth)
|
|
||||||
├── GOG-SETUP.md (GOG Token)
|
|
||||||
├── IOS-WEB-STRATEGY.md (Gesamtvision)
|
|
||||||
└── ARCHITECTURE.md (Tech Details)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Viel Spaß mit WhatToPlay! 🎮**
|
|
||||||
161
README.md
161
README.md
@@ -8,7 +8,52 @@ Eine PWA zum Verwalten deiner Spielebibliotheken von Steam, GOG, Epic, und mehr.
|
|||||||
- 🎮 Steam, GOG, Epic Games, Battle.net Integration
|
- 🎮 Steam, GOG, Epic Games, Battle.net Integration
|
||||||
- 📱 PWA - funktioniert auf iPhone, Android, Desktop
|
- 📱 PWA - funktioniert auf iPhone, Android, Desktop
|
||||||
- 🔒 Daten bleiben lokal (IndexedDB)
|
- 🔒 Daten bleiben lokal (IndexedDB)
|
||||||
- ⚡ Schnelle Tinder-ス タイル Entdeckung
|
- ⚡ Schnelle Tinder-Style Entdeckung
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Die App läuft komplett auf Uberspace (~5€/Monat):
|
||||||
|
- **Frontend**: PWA (statische Files)
|
||||||
|
- **Backend**: Node.js Express Server (CORS-Proxy für Steam API)
|
||||||
|
- **URL**: https://wtp.uber.space
|
||||||
|
|
||||||
|
Details zum Deployment siehe [UBERSPACE.md](UBERSPACE.md).
|
||||||
|
|
||||||
|
## Steam API Integration
|
||||||
|
|
||||||
|
### 1. Steam API Key bekommen
|
||||||
|
|
||||||
|
1. Gehe zu https://steamcommunity.com/dev/apikey
|
||||||
|
2. Akzeptiere die Terms
|
||||||
|
3. Domain: `localhost` (wird ignoriert)
|
||||||
|
4. Kopiere deinen API Key
|
||||||
|
|
||||||
|
### 2. Steam ID finden
|
||||||
|
|
||||||
|
Option A: Steam Profil URL nutzen
|
||||||
|
- `https://steamcommunity.com/id/DEINNAME/` → ID ist `DEINNAME`
|
||||||
|
|
||||||
|
Option B: SteamID Finder
|
||||||
|
- https://steamid.io/
|
||||||
|
|
||||||
|
### 3. In der App konfigurieren
|
||||||
|
|
||||||
|
1. Öffne https://wtp.uber.space
|
||||||
|
2. Gehe zu **Settings → Steam**
|
||||||
|
3. Füge **Steam API Key** und **Steam ID** hinzu
|
||||||
|
4. Klicke auf **Refresh** → Deine Spiele werden geladen! 🎉
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
PWA (wtp.uber.space)
|
||||||
|
↓ POST /api/steam/refresh
|
||||||
|
Express Backend (wtp.uber.space:3000)
|
||||||
|
↓ Forward mit API Key
|
||||||
|
Steam Web API
|
||||||
|
↓ Games List
|
||||||
|
Backend → PWA → IndexedDB
|
||||||
|
```
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
@@ -17,116 +62,7 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production Deployment
|
Der Dev-Server nutzt Vite-Middleware für API-Calls, kein separates Backend nötig.
|
||||||
|
|
||||||
Die App ist deployed unter: https://felixfoertsch.github.io/whattoplay/
|
|
||||||
|
|
||||||
## Steam API auf dem iPhone nutzen
|
|
||||||
|
|
||||||
Die App nutzt Cloudflare Workers als CORS-Proxy für die Steam API. Du kannst deinen eigenen Worker deployen (kostenlos im Free Tier).
|
|
||||||
|
|
||||||
### Option 1: Automatisches In-App Deployment (Empfohlen)
|
|
||||||
|
|
||||||
1. Öffne die App: `https://felixfoertsch.github.io/whattoplay/`
|
|
||||||
2. Gehe zu **Settings → Cloudflare Worker**
|
|
||||||
3. Folge dem Setup-Wizard:
|
|
||||||
- Erstelle CF API Token im Dashboard
|
|
||||||
- Füge Token in App ein
|
|
||||||
- Klicke "Worker deployen"
|
|
||||||
4. ✅ Fertig - Worker ist deployed!
|
|
||||||
5. Gehe zu **Settings → Steam** und nutze die Steam API
|
|
||||||
|
|
||||||
### Option 2: Manuelles CLI Deployment
|
|
||||||
|
|
||||||
Da GitHub Pages statisch ist, kannst du die Steam API nicht direkt aufrufen. Deploye stattdessen deinen eigenen Cloudflare Worker (kostenlos):
|
|
||||||
|
|
||||||
**Deploy deinen Worker:**
|
|
||||||
|
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/felixfoertsch/whattoplay)
|
|
||||||
|
|
||||||
**Manuelle Alternative:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Wrangler installieren
|
|
||||||
npm install wrangler --save-dev
|
|
||||||
|
|
||||||
# Zu Worker Directory wechseln
|
|
||||||
cd workers
|
|
||||||
|
|
||||||
# Worker deployen
|
|
||||||
npx wrangler deploy
|
|
||||||
|
|
||||||
# Deine Worker URL wird angezeigt:
|
|
||||||
# https://whattoplay-api.YOUR_USERNAME.workers.dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Worker URL in der App konfigurieren
|
|
||||||
|
|
||||||
**Bei In-App Deployment**: Worker URL wird automatisch gespeichert ✅
|
|
||||||
|
|
||||||
**Bei manuellem Deployment**:
|
|
||||||
1. Öffne die App auf deinem iPhone
|
|
||||||
2. Gehe zu **Settings → Steam**
|
|
||||||
3. Gebe deine **Worker URL** ein (z.B. `https://whattoplay-api.username.workers.dev`)
|
|
||||||
4. Speichere die Einstellungen
|
|
||||||
5. Füge deinen **Steam API Key** und **Steam ID** hinzu
|
|
||||||
6. Klicke auf **Refresh** → Deine Spiele werden geladen! 🎉
|
|
||||||
|
|
||||||
### Warum Cloudflare Workers?
|
|
||||||
|
|
||||||
- ✅ **100% Kostenlos** (100k requests/Tag im Free Tier)
|
|
||||||
- ✅ **Kein eigenes Hosting** (CF hostet für dich)
|
|
||||||
- ✅ **Automatisches Deployment** aus der App heraus
|
|
||||||
- ✅ **CORS-Proxy** für Steam API
|
|
||||||
- ✅ **Schnell deployed** (~2 Minuten)
|
|
||||||
|
|
||||||
### 3. Steam API Key bekommen
|
|
||||||
|
|
||||||
1. Gehe zu https://steamcommunity.com/dev/apikey
|
|
||||||
2. Akzeptiere die Terms
|
|
||||||
3. Domain: `localhost` (wird ignoriert)
|
|
||||||
4. Kopiere deinen API Key
|
|
||||||
|
|
||||||
### 4. Steam ID finden
|
|
||||||
|
|
||||||
Option A: Steam Profil URL nutzen
|
|
||||||
|
|
||||||
- `https://steamcommunity.com/id/DEINNAME/` → ID ist `DEINNAME`
|
|
||||||
|
|
||||||
Option B: SteamID Finder
|
|
||||||
|
|
||||||
- https://steamid.io/
|
|
||||||
|
|
||||||
## Architektur
|
|
||||||
|
|
||||||
```
|
|
||||||
iPhone App (GitHub Pages)
|
|
||||||
↓ POST /api/steam/refresh
|
|
||||||
Cloudflare Worker (dein eigener)
|
|
||||||
↓ Forward mit API Key
|
|
||||||
Steam Web API
|
|
||||||
↓ Games List
|
|
||||||
Worker → App → IndexedDB
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wichtig:**
|
|
||||||
|
|
||||||
- Jeder User deployed seinen eigenen Worker
|
|
||||||
- API Keys bleiben client-seitig
|
|
||||||
- Worker ist nur ein CORS-Proxy
|
|
||||||
- 100k requests/Tag im Free Tier
|
|
||||||
|
|
||||||
## Development vs Production
|
|
||||||
|
|
||||||
**Development (`npm run dev`):**
|
|
||||||
|
|
||||||
- Vite Dev Server Middleware handled API Calls
|
|
||||||
- Keine Worker URL nötig
|
|
||||||
|
|
||||||
**Production (GitHub Pages):**
|
|
||||||
|
|
||||||
- Worker URL erforderlich
|
|
||||||
- API Calls gehen zu deinem Worker
|
|
||||||
|
|
||||||
## Weitere Plattformen
|
## Weitere Plattformen
|
||||||
|
|
||||||
@@ -140,7 +76,8 @@ Worker → App → IndexedDB
|
|||||||
- Ionic Framework (Mobile UI)
|
- Ionic Framework (Mobile UI)
|
||||||
- IndexedDB (lokale Persistenz)
|
- IndexedDB (lokale Persistenz)
|
||||||
- Vite (Build Tool)
|
- Vite (Build Tool)
|
||||||
- Cloudflare Workers (Backend)
|
- Node.js Express (Backend)
|
||||||
|
- Uberspace (Hosting)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,421 +0,0 @@
|
|||||||
# Cloudflare Workers - Serverless OAuth Proxy
|
|
||||||
|
|
||||||
**Zero Infrastruktur, alles gekapselt** - So funktioniert der Proxy für GOG und Blizzard OAuth Flows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Überblick
|
|
||||||
|
|
||||||
Statt auf einem eigenen Server zu hosten, nutzen wir **Cloudflare Workers** als serverless FaaS (Function as a Service):
|
|
||||||
|
|
||||||
```
|
|
||||||
WhatToPlay Frontend Cloudflare Worker GOG/Blizzard API
|
|
||||||
↓ ↓ ↓
|
|
||||||
[Settings speichern] → [OAuth Token Exchange] ← [Bearer Token zurück]
|
|
||||||
[API aufrufen] → [Token validieren]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
|
|
||||||
- ✅ Keine Server zu verwalten
|
|
||||||
- ✅ Kein Backend-Hosting nötig
|
|
||||||
- ✅ Client Secrets geschützt (Server-Side)
|
|
||||||
- ✅ Kostenlos bis 100.000 Anfragen/Tag
|
|
||||||
- ✅ Überall deployed (weltweit verteilt)
|
|
||||||
- ✅ Automatische CORS-Konfiguration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Setup Anleitung
|
|
||||||
|
|
||||||
### 1. Cloudflare Account erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Gehe zu https://dash.cloudflare.com
|
|
||||||
# Registriere dich kostenfrei
|
|
||||||
# Du brauchst keine Domain für Workers!
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Wrangler installieren (CLI Tool)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -D wrangler
|
|
||||||
npx wrangler login
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Projekt initialisieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd whattoplay
|
|
||||||
npx wrangler init workers
|
|
||||||
# oder für bestehendes Projekt:
|
|
||||||
# npx wrangler init whattoplay-oauth --type javascript
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 GOG OAuth Worker
|
|
||||||
|
|
||||||
### Create `workers/gog-auth.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/**
|
|
||||||
* GOG OAuth Proxy for WhatToPlay
|
|
||||||
* Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/gog/callback
|
|
||||||
*/
|
|
||||||
|
|
||||||
const GOG_CLIENT_ID = "your_client_id";
|
|
||||||
const GOG_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store (nicht in Code!)
|
|
||||||
const GOG_REDIRECT_URI =
|
|
||||||
"https://whattoplay-oauth.your-domain.workers.dev/gog/callback";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async fetch(request) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
// CORS Headers
|
|
||||||
const headers = {
|
|
||||||
"Access-Control-Allow-Origin": "https://whattoplay.local",
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preflight
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return new Response(null, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Initiiere OAuth Flow
|
|
||||||
if (url.pathname === "/gog/authorize") {
|
|
||||||
const authUrl = new URL("https://auth.gog.com/auth");
|
|
||||||
authUrl.searchParams.append("client_id", GOG_CLIENT_ID);
|
|
||||||
authUrl.searchParams.append("redirect_uri", GOG_REDIRECT_URI);
|
|
||||||
authUrl.searchParams.append("response_type", "code");
|
|
||||||
authUrl.searchParams.append("layout", "client2");
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: { Location: authUrl.toString() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Callback Handler
|
|
||||||
if (url.pathname === "/gog/callback") {
|
|
||||||
const code = url.searchParams.get("code");
|
|
||||||
if (!code) {
|
|
||||||
return new Response("Missing authorization code", {
|
|
||||||
status: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Token Exchange (Server-Side!)
|
|
||||||
const tokenResponse = await fetch("https://auth.gog.com/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
client_id: GOG_CLIENT_ID,
|
|
||||||
client_secret: GOG_CLIENT_SECRET, // 🔒 Sicher!
|
|
||||||
grant_type: "authorization_code",
|
|
||||||
code: code,
|
|
||||||
redirect_uri: GOG_REDIRECT_URI,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
|
|
||||||
// Redirect zurück zur App mit Token
|
|
||||||
const appRedirect = `https://whattoplay.local/#/settings?gog_token=${tokenData.access_token}&gog_user=${tokenData.user_id}`;
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: { Location: appRedirect },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(`Token Error: ${error.message}`, {
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Token Validation
|
|
||||||
if (url.pathname === "/gog/validate") {
|
|
||||||
const authHeader = request.headers.get("Authorization");
|
|
||||||
if (!authHeader) {
|
|
||||||
return new Response("Missing Authorization", {
|
|
||||||
status: 401,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.replace("Bearer ", "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
"https://galaxy-library.gog.com/users/me",
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
return new Response(JSON.stringify({ valid: true, user: data }), {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify({ valid: false }), {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ valid: false, error: error.message }),
|
|
||||||
{
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### `wrangler.toml` Config:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
name = "whattoplay-oauth"
|
|
||||||
main = "src/index.js"
|
|
||||||
compatibility_date = "2024-01-01"
|
|
||||||
|
|
||||||
# KV Store für Secrets
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "SECRETS"
|
|
||||||
id = "your_kv_namespace_id"
|
|
||||||
preview_id = "your_preview_kv_id"
|
|
||||||
|
|
||||||
# Environment Variables (Secrets!)
|
|
||||||
[env.production]
|
|
||||||
vars = { ENVIRONMENT = "production" }
|
|
||||||
|
|
||||||
[env.production.secrets]
|
|
||||||
GOG_CLIENT_SECRET = "your_client_secret"
|
|
||||||
BLIZZARD_CLIENT_SECRET = "your_client_secret"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎮 Blizzard OAuth Worker
|
|
||||||
|
|
||||||
### Create `workers/blizzard-auth.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/**
|
|
||||||
* Blizzard OAuth Proxy for WhatToPlay
|
|
||||||
* Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BLIZZARD_CLIENT_ID = "your_client_id";
|
|
||||||
const BLIZZARD_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store!
|
|
||||||
const BLIZZARD_REDIRECT_URI =
|
|
||||||
"https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async fetch(request) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Access-Control-Allow-Origin": "https://whattoplay.local",
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return new Response(null, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Authorize
|
|
||||||
if (url.pathname === "/blizzard/authorize") {
|
|
||||||
const state = crypto.randomUUID();
|
|
||||||
const authUrl = new URL("https://oauth.battle.net/authorize");
|
|
||||||
authUrl.searchParams.append("client_id", BLIZZARD_CLIENT_ID);
|
|
||||||
authUrl.searchParams.append("redirect_uri", BLIZZARD_REDIRECT_URI);
|
|
||||||
authUrl.searchParams.append("response_type", "code");
|
|
||||||
authUrl.searchParams.append("state", state);
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: { Location: authUrl.toString() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Callback
|
|
||||||
if (url.pathname === "/blizzard/callback") {
|
|
||||||
const code = url.searchParams.get("code");
|
|
||||||
const state = url.searchParams.get("state");
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
return new Response("Missing authorization code", {
|
|
||||||
status: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tokenResponse = await fetch("https://oauth.battle.net/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
client_id: BLIZZARD_CLIENT_ID,
|
|
||||||
client_secret: BLIZZARD_CLIENT_SECRET, // 🔒 Sicher!
|
|
||||||
grant_type: "authorization_code",
|
|
||||||
code: code,
|
|
||||||
redirect_uri: BLIZZARD_REDIRECT_URI,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
|
||||||
throw new Error(`Token request failed: ${tokenResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
|
|
||||||
// Redirect zurück
|
|
||||||
const appRedirect = `https://whattoplay.local/#/settings?blizzard_token=${tokenData.access_token}`;
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: { Location: appRedirect },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(`Error: ${error.message}`, {
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment
|
|
||||||
|
|
||||||
### 1. Deploy zu Cloudflare
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx wrangler deploy workers/gog-auth.js --name whattoplay-gog
|
|
||||||
npx wrangler deploy workers/blizzard-auth.js --name whattoplay-blizzard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Custom Domain (optional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Wenn du einen Domain hast, verbinde Cloudflare:
|
|
||||||
# https://dash.cloudflare.com → Workers Routes
|
|
||||||
|
|
||||||
# Beispiel:
|
|
||||||
# Domain: api.whattoplay.com
|
|
||||||
# Worker: whattoplay-oauth
|
|
||||||
# Route: api.whattoplay.com/gog/*
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Secrets hinzufügen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# GOG Secret
|
|
||||||
echo "your_gog_secret" | npx wrangler secret put GOG_CLIENT_SECRET --name whattoplay-gog
|
|
||||||
|
|
||||||
# Blizzard Secret
|
|
||||||
echo "your_blizzard_secret" | npx wrangler secret put BLIZZARD_CLIENT_SECRET --name whattoplay-blizzard
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Frontend Integration
|
|
||||||
|
|
||||||
In `SettingsPage.tsx`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Button für GOG OAuth Login
|
|
||||||
const handleGogOAuth = () => {
|
|
||||||
const workerUrl = "https://whattoplay-oauth.workers.dev/gog/authorize";
|
|
||||||
window.location.href = workerUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback mit URL-Parametern
|
|
||||||
const handleOAuthCallback = () => {
|
|
||||||
const params = new URLSearchParams(window.location.hash.split("?")[1]);
|
|
||||||
const token = params.get("gog_token");
|
|
||||||
const userId = params.get("gog_user");
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
handleSaveConfig("gog", {
|
|
||||||
accessToken: token,
|
|
||||||
userId: userId,
|
|
||||||
});
|
|
||||||
// Token ist jetzt gespeichert in localStorage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Kosten (Februar 2026)
|
|
||||||
|
|
||||||
| Service | Free Tier | Kosten |
|
|
||||||
| ------------------ | ------------ | ---------------------- |
|
|
||||||
| Cloudflare Workers | 100k req/Tag | $0.50 pro 10M Anfragen |
|
|
||||||
| KV Store | 3GB Storage | $0.50 pro GB |
|
|
||||||
| Bandwidth | Unlimited | Keine Zusatzkosten |
|
|
||||||
|
|
||||||
**Beispiel:** 1.000 Users, je 10 Tokens/Monat = 10.000 Anfragen = **Kostenlos** 🎉
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 Security Best Practices
|
|
||||||
|
|
||||||
### ✅ Was wir tun:
|
|
||||||
|
|
||||||
- Client Secrets in KV Store (nicht im Code)
|
|
||||||
- Token Exchange Server-Side
|
|
||||||
- CORS nur für unsere Domain
|
|
||||||
- State Parameter für CSRF Protection
|
|
||||||
- Keine Tokens in URLs speichern (Session nur)
|
|
||||||
|
|
||||||
### ❌ Was wir NICHT tun:
|
|
||||||
|
|
||||||
- Client Secrets hardcoden
|
|
||||||
- Tokens in localStorage ohne Verschlüsselung
|
|
||||||
- CORS für alle Origins
|
|
||||||
- Tokens in Browser Console anzeigen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Debugging
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Logs anschauen
|
|
||||||
npx wrangler tail whattoplay-gog
|
|
||||||
|
|
||||||
# Local testen
|
|
||||||
npx wrangler dev workers/gog-auth.js
|
|
||||||
# Öffne dann: http://localhost:8787/gog/authorize
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Links
|
|
||||||
|
|
||||||
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
|
|
||||||
- [Wrangler CLI Guide](https://developers.cloudflare.com/workers/wrangler/)
|
|
||||||
- [KV Store Reference](https://developers.cloudflare.com/workers/runtime-apis/kv/)
|
|
||||||
- [GOG OAuth Docs](https://gogapidocs.readthedocs.io/)
|
|
||||||
- [Blizzard OAuth Docs](https://develop.battle.net/documentation/guides/using-oauth)
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# WhatToPlay - iOS/Web Strategie
|
|
||||||
|
|
||||||
## ✅ Was funktioniert JETZT
|
|
||||||
|
|
||||||
### Steam Integration (Voll funktionsfähig)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ Öffentliche Web API - funktioniert im Browser/iOS
|
|
||||||
const response = await fetch(
|
|
||||||
"http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/",
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
key: "YOUR_STEAM_API_KEY",
|
|
||||||
steamid: "YOUR_STEAM_ID",
|
|
||||||
format: "json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Status**: 1103 Games erfolgreich importiert ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Was BACKEND braucht
|
|
||||||
|
|
||||||
### GOG Integration
|
|
||||||
|
|
||||||
**Problem**: OAuth Token Exchange geht nicht im Browser (CORS + Secrets)
|
|
||||||
|
|
||||||
**Development-Lösung** (jetzt):
|
|
||||||
|
|
||||||
1. Öffne https://www.gog.com (eingeloggt)
|
|
||||||
2. Browser DevTools → Network → Kopiere Bearer Token
|
|
||||||
3. Trage in `config.local.json` ein
|
|
||||||
|
|
||||||
**Production-Lösung** (später):
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend → Backend (Vercel Function) → GOG OAuth
|
|
||||||
→ GOG Galaxy Library API
|
|
||||||
```
|
|
||||||
|
|
||||||
**Siehe**: [docs/GOG-SETUP.md](./GOG-SETUP.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Epic Games Integration
|
|
||||||
|
|
||||||
**Problem**: Keine öffentliche API, nur CLI-Tool (Legendary)
|
|
||||||
|
|
||||||
**Optionen**:
|
|
||||||
|
|
||||||
1. ❌ Legendary CLI → Funktioniert nicht auf iOS
|
|
||||||
2. ⚠️ Backend mit Epic GraphQL → Reverse-Engineered, gegen ToS
|
|
||||||
3. ✅ Manuelle Import-Funktion → User uploaded JSON
|
|
||||||
|
|
||||||
**Empfehlung**: Manuelle Import-Funktion für MVP
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Amazon Games Integration
|
|
||||||
|
|
||||||
**Problem**: Keine öffentliche API, nur CLI-Tool (Nile)
|
|
||||||
|
|
||||||
**Status**: Gleiche Situation wie Epic
|
|
||||||
**Empfehlung**: Später, wenn Epic funktioniert
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 MVP Strategie (iOS/Web Ready)
|
|
||||||
|
|
||||||
### Phase 1: Steam Only (✅ Fertig)
|
|
||||||
|
|
||||||
```
|
|
||||||
React/Ionic App
|
|
||||||
↓
|
|
||||||
Steam Web API (direkt vom Browser)
|
|
||||||
↓
|
|
||||||
1103 Games imported
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: GOG mit Backend (🔜 Next)
|
|
||||||
|
|
||||||
```
|
|
||||||
React/Ionic App
|
|
||||||
↓
|
|
||||||
Vercel Function (OAuth Proxy)
|
|
||||||
↓
|
|
||||||
GOG Galaxy Library API
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Epic/Amazon Import (📝 TODO)
|
|
||||||
|
|
||||||
```
|
|
||||||
React/Ionic App
|
|
||||||
↓
|
|
||||||
User uploaded JSON
|
|
||||||
↓
|
|
||||||
Parse & Display
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment Plan
|
|
||||||
|
|
||||||
### Frontend (iOS/Web)
|
|
||||||
|
|
||||||
- **Hosting**: Vercel / Netlify (Static React App)
|
|
||||||
- **PWA**: Service Worker für Offline-Support
|
|
||||||
- **iOS**: Add to Home Screen (keine App Store App)
|
|
||||||
|
|
||||||
### Backend (nur für GOG/Epic OAuth)
|
|
||||||
|
|
||||||
- **Option 1**: Vercel Serverless Functions
|
|
||||||
- **Option 2**: Cloudflare Workers
|
|
||||||
- **Option 3**: Supabase Edge Functions
|
|
||||||
|
|
||||||
### Datenbank (optional)
|
|
||||||
|
|
||||||
- **Option 1**: localStorage (nur Client-Side)
|
|
||||||
- **Option 2**: Supabase (für Cloud-Sync)
|
|
||||||
- **Option 3**: Firebase Firestore
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ FAQ
|
|
||||||
|
|
||||||
### Warum kein Python/CLI auf iOS?
|
|
||||||
|
|
||||||
iOS erlaubt keine nativen Binaries in Web-Apps. Nur JavaScript im Browser oder Swift in nativer App.
|
|
||||||
|
|
||||||
### Warum brauchen wir ein Backend?
|
|
||||||
|
|
||||||
OAuth Secrets können nicht sicher im Browser gespeichert werden (jeder kann den Source-Code sehen). CORS blockiert direkte API-Calls.
|
|
||||||
|
|
||||||
### Kann ich die App ohne Backend nutzen?
|
|
||||||
|
|
||||||
Ja! Steam funktioniert ohne Backend. GOG/Epic brauchen aber Backend oder manuelle Imports.
|
|
||||||
|
|
||||||
### Wie sicher sind die Tokens?
|
|
||||||
|
|
||||||
- **Development**: Tokens in `config.local.json` (nicht in Git!)
|
|
||||||
- **Production**: Tokens im Backend, verschlüsselt in DB
|
|
||||||
- **iOS**: Tokens im Keychain (nativer secure storage)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Checklist
|
|
||||||
|
|
||||||
- [x] Steam API Integration
|
|
||||||
- [x] React/Ionic UI Setup
|
|
||||||
- [x] Tab Navigation (Home, Library, Playlists, Discover, **Settings**)
|
|
||||||
- [x] Game Consolidation (Duplicates merging)
|
|
||||||
- [x] Blizzard API Integration
|
|
||||||
- [x] Settings-Tab mit Tutorials
|
|
||||||
- [x] ConfigService (localStorage + IndexedDB)
|
|
||||||
- [ ] GOG OAuth Backend (Cloudflare Worker)
|
|
||||||
- [ ] Epic Import-Funktion (JSON Upload)
|
|
||||||
- [ ] PWA Setup (Service Worker)
|
|
||||||
- [ ] iOS Testing (Add to Home Screen)
|
|
||||||
- [ ] Cloud-Sync (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Nützliche Links
|
|
||||||
|
|
||||||
- [Steam Web API Docs](https://developer.valvesoftware.com/wiki/Steam_Web_API)
|
|
||||||
- [GOG Galaxy API](https://galaxy-library.gog.com/)
|
|
||||||
- [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (Referenz-Implementation)
|
|
||||||
- [Ionic React Docs](https://ionicframework.com/docs/react)
|
|
||||||
- [PWA Guide](https://web.dev/progressive-web-apps/)
|
|
||||||
@@ -2,13 +2,16 @@
|
|||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteBase /
|
RewriteBase /
|
||||||
|
|
||||||
# Don't rewrite files or directories
|
# Don't rewrite files or directories
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
|
|
||||||
# Don't rewrite API calls
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_URI} !^/api/
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
|
||||||
# Rewrite everything else to index.html
|
# Don't rewrite API calls
|
||||||
RewriteRule . /index.html [L]
|
|
||||||
|
RewriteCond %{REQUEST_URI} !^/api/
|
||||||
|
|
||||||
|
# Rewrite everything else to index.html
|
||||||
|
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cloudflare Worker: Steam API CORS Proxy
|
|
||||||
* Erlaubt iPhone App, Steam Web API aufzurufen
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async fetch(request, env, ctx) {
|
|
||||||
// CORS preflight
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return handleCORS();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur POST /api/steam/refresh erlauben
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (request.method === "POST" && url.pathname === "/api/steam/refresh") {
|
|
||||||
return handleSteamRefresh(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404 für alle anderen Routes
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles Steam API refresh request
|
|
||||||
*/
|
|
||||||
async function handleSteamRefresh(request) {
|
|
||||||
try {
|
|
||||||
// Parse request body
|
|
||||||
const body = await request.json();
|
|
||||||
const { apiKey, steamId } = body;
|
|
||||||
|
|
||||||
if (!apiKey || !steamId) {
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: "apiKey and steamId are required" },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch games from Steam API
|
|
||||||
const { games, count } = await fetchSteamGames(apiKey, steamId);
|
|
||||||
|
|
||||||
return jsonResponse({ games, count });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Steam API Error:", error);
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: error.message || "Internal Server Error" },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches games from Steam Web API
|
|
||||||
*/
|
|
||||||
async function fetchSteamGames(apiKey, steamId) {
|
|
||||||
// Build Steam API URL
|
|
||||||
const url = new URL(
|
|
||||||
"https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/",
|
|
||||||
);
|
|
||||||
url.searchParams.set("key", apiKey);
|
|
||||||
url.searchParams.set("steamid", steamId);
|
|
||||||
url.searchParams.set("include_appinfo", "true");
|
|
||||||
url.searchParams.set("include_played_free_games", "true");
|
|
||||||
|
|
||||||
// Call Steam API
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Steam API Error: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const rawGames = data.response?.games ?? [];
|
|
||||||
|
|
||||||
// Format games
|
|
||||||
const games = rawGames.map((game) => ({
|
|
||||||
id: `steam-${game.appid}`,
|
|
||||||
title: game.name,
|
|
||||||
source: "steam",
|
|
||||||
sourceId: String(game.appid),
|
|
||||||
platform: "PC",
|
|
||||||
lastPlayed: game.rtime_last_played
|
|
||||||
? new Date(game.rtime_last_played * 1000).toISOString().slice(0, 10)
|
|
||||||
: null,
|
|
||||||
playtimeHours: Math.round((game.playtime_forever / 60) * 10) / 10,
|
|
||||||
url: `https://store.steampowered.com/app/${game.appid}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
games,
|
|
||||||
count: games.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CORS preflight response
|
|
||||||
*/
|
|
||||||
function handleCORS() {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 204,
|
|
||||||
headers: getCORSHeaders(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON response with CORS headers
|
|
||||||
*/
|
|
||||||
function jsonResponse(data, options = {}) {
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...getCORSHeaders(),
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CORS headers for GitHub Pages
|
|
||||||
*/
|
|
||||||
function getCORSHeaders() {
|
|
||||||
return {
|
|
||||||
"Access-Control-Allow-Origin": "*", // Allow all origins (user's own worker)
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
"Access-Control-Max-Age": "86400", // 24 hours
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -121,10 +121,7 @@ export default function SettingsDetailPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = ConfigService.getApiUrl(
|
const apiUrl = ConfigService.getApiUrl("/api/steam/refresh");
|
||||||
"/api/steam/refresh",
|
|
||||||
config.workerUrl,
|
|
||||||
);
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import { db } from "./Database";
|
import { db } from "./Database";
|
||||||
|
|
||||||
export interface ServiceConfig {
|
export interface ServiceConfig {
|
||||||
workerUrl?: string;
|
|
||||||
steam?: {
|
steam?: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
steamId?: string;
|
steamId?: string;
|
||||||
@@ -130,33 +129,24 @@ export class ConfigService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get API URL for Steam refresh
|
* Get API URL for Steam refresh
|
||||||
* Supports multiple deployment scenarios:
|
|
||||||
* - Development: Vite dev server proxy
|
* - Development: Vite dev server proxy
|
||||||
* - Uberspace: Backend on same domain via VITE_API_URL
|
* - Production: Uberspace backend via VITE_API_URL
|
||||||
* - Cloudflare Workers: User-configured Worker URL (fallback)
|
|
||||||
*/
|
*/
|
||||||
static getApiUrl(endpoint: string, workerUrl?: string): string {
|
static getApiUrl(endpoint: string): string {
|
||||||
// Development mode: Use Vite dev server middleware
|
// Development mode: Use Vite dev server middleware
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production: Check for backend URL from environment
|
// Production: Use backend URL from environment
|
||||||
const backendUrl = import.meta.env.VITE_API_URL;
|
const backendUrl = import.meta.env.VITE_API_URL;
|
||||||
if (backendUrl) {
|
if (!backendUrl) {
|
||||||
const baseUrl = backendUrl.replace(/\/$/, "");
|
throw new Error(
|
||||||
return `${baseUrl}${endpoint}`;
|
"Backend not configured. Set VITE_API_URL in .env.production",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Cloudflare Worker (if configured)
|
const baseUrl = backendUrl.replace(/\/$/, "");
|
||||||
if (workerUrl) {
|
return `${baseUrl}${endpoint}`;
|
||||||
const baseUrl = workerUrl.replace(/\/$/, "");
|
|
||||||
return `${baseUrl}${endpoint}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No backend configured
|
|
||||||
throw new Error(
|
|
||||||
"Backend not configured. Please deploy the server or set up a Cloudflare Worker.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface DbConfig {
|
export interface DbConfig {
|
||||||
workerUrl?: string; // Cloudflare Worker URL for API proxying
|
|
||||||
cloudflare?: {
|
|
||||||
apiToken?: string; // CF API Token (encrypted)
|
|
||||||
accountId?: string; // CF Account ID
|
|
||||||
accountName?: string; // CF Account Name
|
|
||||||
subdomain?: string; // workers.dev Subdomain
|
|
||||||
workerName?: string; // Deployed Worker Name
|
|
||||||
lastDeployed?: string; // Timestamp
|
|
||||||
};
|
|
||||||
steam?: {
|
steam?: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
steamId?: string;
|
steamId?: string;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
const urls = new Set();
|
|
||||||
|
|
||||||
function checkURL(request, init) {
|
|
||||||
const url =
|
|
||||||
request instanceof URL
|
|
||||||
? request
|
|
||||||
: new URL(
|
|
||||||
(typeof request === "string"
|
|
||||||
? new Request(request, init)
|
|
||||||
: request
|
|
||||||
).url
|
|
||||||
);
|
|
||||||
if (url.port && url.port !== "443" && url.protocol === "https:") {
|
|
||||||
if (!urls.has(url.toString())) {
|
|
||||||
urls.add(url.toString());
|
|
||||||
console.warn(
|
|
||||||
`WARNING: known issue with \`fetch()\` requests to custom HTTPS ports in published Workers:\n` +
|
|
||||||
` - ${url.toString()} - the custom port will be ignored when the Worker is published using the \`wrangler deploy\` command.\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.fetch = new Proxy(globalThis.fetch, {
|
|
||||||
apply(target, thisArg, argArray) {
|
|
||||||
const [request, init] = argArray;
|
|
||||||
checkURL(request, init);
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import worker, * as OTHER_EXPORTS from "/Users/felixfoertsch/Developer/whattoplay/workers/steam-proxy.js";
|
|
||||||
import * as __MIDDLEWARE_0__ from "/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts";
|
|
||||||
import * as __MIDDLEWARE_1__ from "/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts";
|
|
||||||
|
|
||||||
export * from "/Users/felixfoertsch/Developer/whattoplay/workers/steam-proxy.js";
|
|
||||||
const MIDDLEWARE_TEST_INJECT = "__INJECT_FOR_TESTING_WRANGLER_MIDDLEWARE__";
|
|
||||||
export const __INTERNAL_WRANGLER_MIDDLEWARE__ = [
|
|
||||||
|
|
||||||
__MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default
|
|
||||||
]
|
|
||||||
export default worker;
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// This loads all middlewares exposed on the middleware object and then starts
|
|
||||||
// the invocation chain. The big idea is that we can add these to the middleware
|
|
||||||
// export dynamically through wrangler, or we can potentially let users directly
|
|
||||||
// add them as a sort of "plugin" system.
|
|
||||||
|
|
||||||
import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js";
|
|
||||||
import { __facade_invoke__, __facade_register__, Dispatcher } from "/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/common.ts";
|
|
||||||
import type { WorkerEntrypointConstructor } from "/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js";
|
|
||||||
|
|
||||||
// Preserve all the exports from the worker
|
|
||||||
export * from "/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js";
|
|
||||||
|
|
||||||
class __Facade_ScheduledController__ implements ScheduledController {
|
|
||||||
readonly #noRetry: ScheduledController["noRetry"];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly scheduledTime: number,
|
|
||||||
readonly cron: string,
|
|
||||||
noRetry: ScheduledController["noRetry"]
|
|
||||||
) {
|
|
||||||
this.#noRetry = noRetry;
|
|
||||||
}
|
|
||||||
|
|
||||||
noRetry() {
|
|
||||||
if (!(this instanceof __Facade_ScheduledController__)) {
|
|
||||||
throw new TypeError("Illegal invocation");
|
|
||||||
}
|
|
||||||
// Need to call native method immediately in case uncaught error thrown
|
|
||||||
this.#noRetry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapExportedHandler(worker: ExportedHandler): ExportedHandler {
|
|
||||||
// If we don't have any middleware defined, just return the handler as is
|
|
||||||
if (
|
|
||||||
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
|
|
||||||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
|
|
||||||
) {
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
// Otherwise, register all middleware once
|
|
||||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
|
||||||
__facade_register__(middleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchDispatcher: ExportedHandlerFetchHandler = function (
|
|
||||||
request,
|
|
||||||
env,
|
|
||||||
ctx
|
|
||||||
) {
|
|
||||||
if (worker.fetch === undefined) {
|
|
||||||
throw new Error("Handler does not export a fetch() function.");
|
|
||||||
}
|
|
||||||
return worker.fetch(request, env, ctx);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...worker,
|
|
||||||
fetch(request, env, ctx) {
|
|
||||||
const dispatcher: Dispatcher = function (type, init) {
|
|
||||||
if (type === "scheduled" && worker.scheduled !== undefined) {
|
|
||||||
const controller = new __Facade_ScheduledController__(
|
|
||||||
Date.now(),
|
|
||||||
init.cron ?? "",
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
return worker.scheduled(controller, env, ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapWorkerEntrypoint(
|
|
||||||
klass: WorkerEntrypointConstructor
|
|
||||||
): WorkerEntrypointConstructor {
|
|
||||||
// If we don't have any middleware defined, just return the handler as is
|
|
||||||
if (
|
|
||||||
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
|
|
||||||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
|
|
||||||
) {
|
|
||||||
return klass;
|
|
||||||
}
|
|
||||||
// Otherwise, register all middleware once
|
|
||||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
|
||||||
__facade_register__(middleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `extend`ing `klass` here so other RPC methods remain callable
|
|
||||||
return class extends klass {
|
|
||||||
#fetchDispatcher: ExportedHandlerFetchHandler<Record<string, unknown>> = (
|
|
||||||
request,
|
|
||||||
env,
|
|
||||||
ctx
|
|
||||||
) => {
|
|
||||||
this.env = env;
|
|
||||||
this.ctx = ctx;
|
|
||||||
if (super.fetch === undefined) {
|
|
||||||
throw new Error("Entrypoint class does not define a fetch() function.");
|
|
||||||
}
|
|
||||||
return super.fetch(request);
|
|
||||||
};
|
|
||||||
|
|
||||||
#dispatcher: Dispatcher = (type, init) => {
|
|
||||||
if (type === "scheduled" && super.scheduled !== undefined) {
|
|
||||||
const controller = new __Facade_ScheduledController__(
|
|
||||||
Date.now(),
|
|
||||||
init.cron ?? "",
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
return super.scheduled(controller);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch(request: Request<unknown, IncomingRequestCfProperties>) {
|
|
||||||
return __facade_invoke__(
|
|
||||||
request,
|
|
||||||
this.env,
|
|
||||||
this.ctx,
|
|
||||||
this.#dispatcher,
|
|
||||||
this.#fetchDispatcher
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;
|
|
||||||
if (typeof ENTRY === "object") {
|
|
||||||
WRAPPED_ENTRY = wrapExportedHandler(ENTRY);
|
|
||||||
} else if (typeof ENTRY === "function") {
|
|
||||||
WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);
|
|
||||||
}
|
|
||||||
export default WRAPPED_ENTRY;
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
var __defProp = Object.defineProperty;
|
|
||||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
||||||
|
|
||||||
// .wrangler/tmp/bundle-QI6oiq/checked-fetch.js
|
|
||||||
var urls = /* @__PURE__ */ new Set();
|
|
||||||
function checkURL(request, init) {
|
|
||||||
const url = request instanceof URL ? request : new URL(
|
|
||||||
(typeof request === "string" ? new Request(request, init) : request).url
|
|
||||||
);
|
|
||||||
if (url.port && url.port !== "443" && url.protocol === "https:") {
|
|
||||||
if (!urls.has(url.toString())) {
|
|
||||||
urls.add(url.toString());
|
|
||||||
console.warn(
|
|
||||||
`WARNING: known issue with \`fetch()\` requests to custom HTTPS ports in published Workers:
|
|
||||||
- ${url.toString()} - the custom port will be ignored when the Worker is published using the \`wrangler deploy\` command.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
__name(checkURL, "checkURL");
|
|
||||||
globalThis.fetch = new Proxy(globalThis.fetch, {
|
|
||||||
apply(target, thisArg, argArray) {
|
|
||||||
const [request, init] = argArray;
|
|
||||||
checkURL(request, init);
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// steam-proxy.js
|
|
||||||
var steam_proxy_default = {
|
|
||||||
async fetch(request, env, ctx) {
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return handleCORS();
|
|
||||||
}
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (request.method === "POST" && url.pathname === "/api/steam/refresh") {
|
|
||||||
return handleSteamRefresh(request);
|
|
||||||
}
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
async function handleSteamRefresh(request) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
const { apiKey, steamId } = body;
|
|
||||||
if (!apiKey || !steamId) {
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: "apiKey and steamId are required" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { games, count } = await fetchSteamGames(apiKey, steamId);
|
|
||||||
return jsonResponse({ games, count });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Steam API Error:", error);
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: error.message || "Internal Server Error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
__name(handleSteamRefresh, "handleSteamRefresh");
|
|
||||||
async function fetchSteamGames(apiKey, steamId) {
|
|
||||||
const url = new URL(
|
|
||||||
"https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/"
|
|
||||||
);
|
|
||||||
url.searchParams.set("key", apiKey);
|
|
||||||
url.searchParams.set("steamid", steamId);
|
|
||||||
url.searchParams.set("include_appinfo", "true");
|
|
||||||
url.searchParams.set("include_played_free_games", "true");
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Steam API Error: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
const rawGames = data.response?.games ?? [];
|
|
||||||
const games = rawGames.map((game) => ({
|
|
||||||
id: `steam-${game.appid}`,
|
|
||||||
title: game.name,
|
|
||||||
source: "steam",
|
|
||||||
sourceId: String(game.appid),
|
|
||||||
platform: "PC",
|
|
||||||
lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1e3).toISOString().slice(0, 10) : null,
|
|
||||||
playtimeHours: Math.round(game.playtime_forever / 60 * 10) / 10,
|
|
||||||
url: `https://store.steampowered.com/app/${game.appid}`
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
games,
|
|
||||||
count: games.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
__name(fetchSteamGames, "fetchSteamGames");
|
|
||||||
function handleCORS() {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 204,
|
|
||||||
headers: getCORSHeaders()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
__name(handleCORS, "handleCORS");
|
|
||||||
function jsonResponse(data, options = {}) {
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...getCORSHeaders(),
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
__name(jsonResponse, "jsonResponse");
|
|
||||||
function getCORSHeaders() {
|
|
||||||
return {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
// Allow all origins (user's own worker)
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
"Access-Control-Max-Age": "86400"
|
|
||||||
// 24 hours
|
|
||||||
};
|
|
||||||
}
|
|
||||||
__name(getCORSHeaders, "getCORSHeaders");
|
|
||||||
|
|
||||||
// ../node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts
|
|
||||||
var drainBody = /* @__PURE__ */ __name(async (request, env, _ctx, middlewareCtx) => {
|
|
||||||
try {
|
|
||||||
return await middlewareCtx.next(request, env);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (request.body !== null && !request.bodyUsed) {
|
|
||||||
const reader = request.body.getReader();
|
|
||||||
while (!(await reader.read()).done) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to drain the unused request body.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, "drainBody");
|
|
||||||
var middleware_ensure_req_body_drained_default = drainBody;
|
|
||||||
|
|
||||||
// ../node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts
|
|
||||||
function reduceError(e) {
|
|
||||||
return {
|
|
||||||
name: e?.name,
|
|
||||||
message: e?.message ?? String(e),
|
|
||||||
stack: e?.stack,
|
|
||||||
cause: e?.cause === void 0 ? void 0 : reduceError(e.cause)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
__name(reduceError, "reduceError");
|
|
||||||
var jsonError = /* @__PURE__ */ __name(async (request, env, _ctx, middlewareCtx) => {
|
|
||||||
try {
|
|
||||||
return await middlewareCtx.next(request, env);
|
|
||||||
} catch (e) {
|
|
||||||
const error = reduceError(e);
|
|
||||||
return Response.json(error, {
|
|
||||||
status: 500,
|
|
||||||
headers: { "MF-Experimental-Error-Stack": "true" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, "jsonError");
|
|
||||||
var middleware_miniflare3_json_error_default = jsonError;
|
|
||||||
|
|
||||||
// .wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js
|
|
||||||
var __INTERNAL_WRANGLER_MIDDLEWARE__ = [
|
|
||||||
middleware_ensure_req_body_drained_default,
|
|
||||||
middleware_miniflare3_json_error_default
|
|
||||||
];
|
|
||||||
var middleware_insertion_facade_default = steam_proxy_default;
|
|
||||||
|
|
||||||
// ../node_modules/wrangler/templates/middleware/common.ts
|
|
||||||
var __facade_middleware__ = [];
|
|
||||||
function __facade_register__(...args) {
|
|
||||||
__facade_middleware__.push(...args.flat());
|
|
||||||
}
|
|
||||||
__name(__facade_register__, "__facade_register__");
|
|
||||||
function __facade_invokeChain__(request, env, ctx, dispatch, middlewareChain) {
|
|
||||||
const [head, ...tail] = middlewareChain;
|
|
||||||
const middlewareCtx = {
|
|
||||||
dispatch,
|
|
||||||
next(newRequest, newEnv) {
|
|
||||||
return __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return head(request, env, ctx, middlewareCtx);
|
|
||||||
}
|
|
||||||
__name(__facade_invokeChain__, "__facade_invokeChain__");
|
|
||||||
function __facade_invoke__(request, env, ctx, dispatch, finalMiddleware) {
|
|
||||||
return __facade_invokeChain__(request, env, ctx, dispatch, [
|
|
||||||
...__facade_middleware__,
|
|
||||||
finalMiddleware
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
__name(__facade_invoke__, "__facade_invoke__");
|
|
||||||
|
|
||||||
// .wrangler/tmp/bundle-QI6oiq/middleware-loader.entry.ts
|
|
||||||
var __Facade_ScheduledController__ = class ___Facade_ScheduledController__ {
|
|
||||||
constructor(scheduledTime, cron, noRetry) {
|
|
||||||
this.scheduledTime = scheduledTime;
|
|
||||||
this.cron = cron;
|
|
||||||
this.#noRetry = noRetry;
|
|
||||||
}
|
|
||||||
static {
|
|
||||||
__name(this, "__Facade_ScheduledController__");
|
|
||||||
}
|
|
||||||
#noRetry;
|
|
||||||
noRetry() {
|
|
||||||
if (!(this instanceof ___Facade_ScheduledController__)) {
|
|
||||||
throw new TypeError("Illegal invocation");
|
|
||||||
}
|
|
||||||
this.#noRetry();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function wrapExportedHandler(worker) {
|
|
||||||
if (__INTERNAL_WRANGLER_MIDDLEWARE__ === void 0 || __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0) {
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
|
||||||
__facade_register__(middleware);
|
|
||||||
}
|
|
||||||
const fetchDispatcher = /* @__PURE__ */ __name(function(request, env, ctx) {
|
|
||||||
if (worker.fetch === void 0) {
|
|
||||||
throw new Error("Handler does not export a fetch() function.");
|
|
||||||
}
|
|
||||||
return worker.fetch(request, env, ctx);
|
|
||||||
}, "fetchDispatcher");
|
|
||||||
return {
|
|
||||||
...worker,
|
|
||||||
fetch(request, env, ctx) {
|
|
||||||
const dispatcher = /* @__PURE__ */ __name(function(type, init) {
|
|
||||||
if (type === "scheduled" && worker.scheduled !== void 0) {
|
|
||||||
const controller = new __Facade_ScheduledController__(
|
|
||||||
Date.now(),
|
|
||||||
init.cron ?? "",
|
|
||||||
() => {
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return worker.scheduled(controller, env, ctx);
|
|
||||||
}
|
|
||||||
}, "dispatcher");
|
|
||||||
return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
__name(wrapExportedHandler, "wrapExportedHandler");
|
|
||||||
function wrapWorkerEntrypoint(klass) {
|
|
||||||
if (__INTERNAL_WRANGLER_MIDDLEWARE__ === void 0 || __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0) {
|
|
||||||
return klass;
|
|
||||||
}
|
|
||||||
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
|
|
||||||
__facade_register__(middleware);
|
|
||||||
}
|
|
||||||
return class extends klass {
|
|
||||||
#fetchDispatcher = /* @__PURE__ */ __name((request, env, ctx) => {
|
|
||||||
this.env = env;
|
|
||||||
this.ctx = ctx;
|
|
||||||
if (super.fetch === void 0) {
|
|
||||||
throw new Error("Entrypoint class does not define a fetch() function.");
|
|
||||||
}
|
|
||||||
return super.fetch(request);
|
|
||||||
}, "#fetchDispatcher");
|
|
||||||
#dispatcher = /* @__PURE__ */ __name((type, init) => {
|
|
||||||
if (type === "scheduled" && super.scheduled !== void 0) {
|
|
||||||
const controller = new __Facade_ScheduledController__(
|
|
||||||
Date.now(),
|
|
||||||
init.cron ?? "",
|
|
||||||
() => {
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return super.scheduled(controller);
|
|
||||||
}
|
|
||||||
}, "#dispatcher");
|
|
||||||
fetch(request) {
|
|
||||||
return __facade_invoke__(
|
|
||||||
request,
|
|
||||||
this.env,
|
|
||||||
this.ctx,
|
|
||||||
this.#dispatcher,
|
|
||||||
this.#fetchDispatcher
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
__name(wrapWorkerEntrypoint, "wrapWorkerEntrypoint");
|
|
||||||
var WRAPPED_ENTRY;
|
|
||||||
if (typeof middleware_insertion_facade_default === "object") {
|
|
||||||
WRAPPED_ENTRY = wrapExportedHandler(middleware_insertion_facade_default);
|
|
||||||
} else if (typeof middleware_insertion_facade_default === "function") {
|
|
||||||
WRAPPED_ENTRY = wrapWorkerEntrypoint(middleware_insertion_facade_default);
|
|
||||||
}
|
|
||||||
var middleware_loader_entry_default = WRAPPED_ENTRY;
|
|
||||||
export {
|
|
||||||
__INTERNAL_WRANGLER_MIDDLEWARE__,
|
|
||||||
middleware_loader_entry_default as default
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=steam-proxy.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,132 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cloudflare Worker: Steam API CORS Proxy
|
|
||||||
* Erlaubt iPhone App, Steam Web API aufzurufen
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async fetch(request, env, ctx) {
|
|
||||||
// CORS preflight
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return handleCORS();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur POST /api/steam/refresh erlauben
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (request.method === "POST" && url.pathname === "/api/steam/refresh") {
|
|
||||||
return handleSteamRefresh(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404 für alle anderen Routes
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles Steam API refresh request
|
|
||||||
*/
|
|
||||||
async function handleSteamRefresh(request) {
|
|
||||||
try {
|
|
||||||
// Parse request body
|
|
||||||
const body = await request.json();
|
|
||||||
const { apiKey, steamId } = body;
|
|
||||||
|
|
||||||
if (!apiKey || !steamId) {
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: "apiKey and steamId are required" },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch games from Steam API
|
|
||||||
const { games, count } = await fetchSteamGames(apiKey, steamId);
|
|
||||||
|
|
||||||
return jsonResponse({ games, count });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Steam API Error:", error);
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: error.message || "Internal Server Error" },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches games from Steam Web API
|
|
||||||
*/
|
|
||||||
async function fetchSteamGames(apiKey, steamId) {
|
|
||||||
// Build Steam API URL
|
|
||||||
const url = new URL(
|
|
||||||
"https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/",
|
|
||||||
);
|
|
||||||
url.searchParams.set("key", apiKey);
|
|
||||||
url.searchParams.set("steamid", steamId);
|
|
||||||
url.searchParams.set("include_appinfo", "true");
|
|
||||||
url.searchParams.set("include_played_free_games", "true");
|
|
||||||
|
|
||||||
// Call Steam API
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Steam API Error: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const rawGames = data.response?.games ?? [];
|
|
||||||
|
|
||||||
// Format games
|
|
||||||
const games = rawGames.map((game) => ({
|
|
||||||
id: `steam-${game.appid}`,
|
|
||||||
title: game.name,
|
|
||||||
source: "steam",
|
|
||||||
sourceId: String(game.appid),
|
|
||||||
platform: "PC",
|
|
||||||
lastPlayed: game.rtime_last_played
|
|
||||||
? new Date(game.rtime_last_played * 1000).toISOString().slice(0, 10)
|
|
||||||
: null,
|
|
||||||
playtimeHours: Math.round((game.playtime_forever / 60) * 10) / 10,
|
|
||||||
url: `https://store.steampowered.com/app/${game.appid}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
games,
|
|
||||||
count: games.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CORS preflight response
|
|
||||||
*/
|
|
||||||
function handleCORS() {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 204,
|
|
||||||
headers: getCORSHeaders(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON response with CORS headers
|
|
||||||
*/
|
|
||||||
function jsonResponse(data, options = {}) {
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...getCORSHeaders(),
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CORS headers for GitHub Pages
|
|
||||||
*/
|
|
||||||
function getCORSHeaders() {
|
|
||||||
return {
|
|
||||||
"Access-Control-Allow-Origin": "*", // Allow all origins (user's own worker)
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
"Access-Control-Max-Age": "86400", // 24 hours
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
name = "whattoplay-api"
|
|
||||||
main = "steam-proxy.js"
|
|
||||||
compatibility_date = "2024-01-01"
|
|
||||||
|
|
||||||
# Account ID wird beim Deploy automatisch gesetzt
|
|
||||||
# account_id = "YOUR_ACCOUNT_ID"
|
|
||||||
|
|
||||||
[observability]
|
|
||||||
enabled = true
|
|
||||||
Reference in New Issue
Block a user