diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index a3e1c5e..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -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. diff --git a/IMPLEMENTATION-SUMMARY.md b/IMPLEMENTATION-SUMMARY.md deleted file mode 100644 index 04d3626..0000000 --- a/IMPLEMENTATION-SUMMARY.md +++ /dev/null @@ -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 diff --git a/QUICK-START.md b/QUICK-START.md deleted file mode 100644 index 7f31f0c..0000000 --- a/QUICK-START.md +++ /dev/null @@ -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! 🎮** diff --git a/README.md b/README.md index 97c8ae3..24ebf9e 100644 --- a/README.md +++ b/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 - 📱 PWA - funktioniert auf iPhone, Android, Desktop - 🔒 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 @@ -17,116 +62,7 @@ npm install npm run dev ``` -## Production Deployment - -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:** - -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](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 +Der Dev-Server nutzt Vite-Middleware für API-Calls, kein separates Backend nötig. ## Weitere Plattformen @@ -140,7 +76,8 @@ Worker → App → IndexedDB - Ionic Framework (Mobile UI) - IndexedDB (lokale Persistenz) - Vite (Build Tool) -- Cloudflare Workers (Backend) +- Node.js Express (Backend) +- Uberspace (Hosting) ## License diff --git a/docs/CLOUDFLARE-WORKERS-SETUP.md b/docs/CLOUDFLARE-WORKERS-SETUP.md deleted file mode 100644 index 9b6ea39..0000000 --- a/docs/CLOUDFLARE-WORKERS-SETUP.md +++ /dev/null @@ -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) diff --git a/docs/IOS-WEB-STRATEGY.md b/docs/IOS-WEB-STRATEGY.md deleted file mode 100644 index 0f76109..0000000 --- a/docs/IOS-WEB-STRATEGY.md +++ /dev/null @@ -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/) diff --git a/public/.htaccess b/public/.htaccess index 31f7dc8..08e18e5 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -2,13 +2,16 @@ RewriteEngine On RewriteBase / - # Don't rewrite files or directories - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d +# Don't rewrite files or directories - # Don't rewrite API calls - RewriteCond %{REQUEST_URI} !^/api/ +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d - # Rewrite everything else to index.html - RewriteRule . /index.html [L] +# Don't rewrite API calls + +RewriteCond %{REQUEST_URI} !^/api/ + +# Rewrite everything else to index.html + +RewriteRule . /index.html [L] diff --git a/public/workers/steam-proxy.js b/public/workers/steam-proxy.js deleted file mode 100644 index db85f81..0000000 --- a/public/workers/steam-proxy.js +++ /dev/null @@ -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 - }; -} diff --git a/src/pages/Settings/SettingsDetailPage.tsx b/src/pages/Settings/SettingsDetailPage.tsx index 7d91cd9..156ff51 100644 --- a/src/pages/Settings/SettingsDetailPage.tsx +++ b/src/pages/Settings/SettingsDetailPage.tsx @@ -121,10 +121,7 @@ export default function SettingsDetailPage() { return; } - const apiUrl = ConfigService.getApiUrl( - "/api/steam/refresh", - config.workerUrl, - ); + const apiUrl = ConfigService.getApiUrl("/api/steam/refresh"); const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts index 1099755..a19b817 100644 --- a/src/services/ConfigService.ts +++ b/src/services/ConfigService.ts @@ -5,7 +5,6 @@ import { db } from "./Database"; export interface ServiceConfig { - workerUrl?: string; steam?: { apiKey?: string; steamId?: string; @@ -130,33 +129,24 @@ export class ConfigService { /** * Get API URL for Steam refresh - * Supports multiple deployment scenarios: * - Development: Vite dev server proxy - * - Uberspace: Backend on same domain via VITE_API_URL - * - Cloudflare Workers: User-configured Worker URL (fallback) + * - Production: Uberspace backend via VITE_API_URL */ - static getApiUrl(endpoint: string, workerUrl?: string): string { + static getApiUrl(endpoint: string): string { // Development mode: Use Vite dev server middleware if (import.meta.env.DEV) { return endpoint; } - // Production: Check for backend URL from environment + // Production: Use backend URL from environment const backendUrl = import.meta.env.VITE_API_URL; - if (backendUrl) { - const baseUrl = backendUrl.replace(/\/$/, ""); - return `${baseUrl}${endpoint}`; + if (!backendUrl) { + throw new Error( + "Backend not configured. Set VITE_API_URL in .env.production", + ); } - // Fallback: Cloudflare Worker (if configured) - if (workerUrl) { - 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.", - ); + const baseUrl = backendUrl.replace(/\/$/, ""); + return `${baseUrl}${endpoint}`; } } diff --git a/src/services/Database.ts b/src/services/Database.ts index 622b74b..da7e274 100644 --- a/src/services/Database.ts +++ b/src/services/Database.ts @@ -4,15 +4,6 @@ */ 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?: { apiKey?: string; steamId?: string; diff --git a/workers/.wrangler/tmp/bundle-QI6oiq/checked-fetch.js b/workers/.wrangler/tmp/bundle-QI6oiq/checked-fetch.js deleted file mode 100644 index 8c007fd..0000000 --- a/workers/.wrangler/tmp/bundle-QI6oiq/checked-fetch.js +++ /dev/null @@ -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); - }, -}); diff --git a/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js b/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js deleted file mode 100644 index bee7539..0000000 --- a/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/workers/.wrangler/tmp/bundle-QI6oiq/middleware-loader.entry.ts b/workers/.wrangler/tmp/bundle-QI6oiq/middleware-loader.entry.ts deleted file mode 100644 index 51e19e8..0000000 --- a/workers/.wrangler/tmp/bundle-QI6oiq/middleware-loader.entry.ts +++ /dev/null @@ -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> = ( - 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) { - 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; diff --git a/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js b/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js deleted file mode 100644 index 718ce00..0000000 --- a/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js +++ /dev/null @@ -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 diff --git a/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js.map b/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js.map deleted file mode 100644 index acf357e..0000000 --- a/workers/.wrangler/tmp/dev-FOcP4T/steam-proxy.js.map +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 3, - "sources": ["../bundle-QI6oiq/checked-fetch.js", "../../../steam-proxy.js", "../../../../node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts", "../../../../node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts", "../bundle-QI6oiq/middleware-insertion-facade.js", "../../../../node_modules/wrangler/templates/middleware/common.ts", "../bundle-QI6oiq/middleware-loader.entry.ts"], - "sourceRoot": "/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/dev-FOcP4T", - "sourcesContent": ["const urls = new Set();\n\nfunction checkURL(request, init) {\n\tconst url =\n\t\trequest instanceof URL\n\t\t\t? request\n\t\t\t: new URL(\n\t\t\t\t\t(typeof request === \"string\"\n\t\t\t\t\t\t? new Request(request, init)\n\t\t\t\t\t\t: request\n\t\t\t\t\t).url\n\t\t\t\t);\n\tif (url.port && url.port !== \"443\" && url.protocol === \"https:\") {\n\t\tif (!urls.has(url.toString())) {\n\t\t\turls.add(url.toString());\n\t\t\tconsole.warn(\n\t\t\t\t`WARNING: known issue with \\`fetch()\\` requests to custom HTTPS ports in published Workers:\\n` +\n\t\t\t\t\t` - ${url.toString()} - the custom port will be ignored when the Worker is published using the \\`wrangler deploy\\` command.\\n`\n\t\t\t);\n\t\t}\n\t}\n}\n\nglobalThis.fetch = new Proxy(globalThis.fetch, {\n\tapply(target, thisArg, argArray) {\n\t\tconst [request, init] = argArray;\n\t\tcheckURL(request, init);\n\t\treturn Reflect.apply(target, thisArg, argArray);\n\t},\n});\n", "/**\n * Cloudflare Worker: Steam API CORS Proxy\n * Erlaubt iPhone App, Steam Web API aufzurufen\n */\n\nexport default {\n\tasync fetch(request, env, ctx) {\n\t\t// CORS preflight\n\t\tif (request.method === \"OPTIONS\") {\n\t\t\treturn handleCORS();\n\t\t}\n\n\t\t// Nur POST /api/steam/refresh erlauben\n\t\tconst url = new URL(request.url);\n\t\tif (request.method === \"POST\" && url.pathname === \"/api/steam/refresh\") {\n\t\t\treturn handleSteamRefresh(request);\n\t\t}\n\n\t\t// 404 f\u00FCr alle anderen Routes\n\t\treturn new Response(\"Not Found\", { status: 404 });\n\t},\n};\n\n/**\n * Handles Steam API refresh request\n */\nasync function handleSteamRefresh(request) {\n\ttry {\n\t\t// Parse request body\n\t\tconst body = await request.json();\n\t\tconst { apiKey, steamId } = body;\n\n\t\tif (!apiKey || !steamId) {\n\t\t\treturn jsonResponse(\n\t\t\t\t{ error: \"apiKey and steamId are required\" },\n\t\t\t\t{ status: 400 },\n\t\t\t);\n\t\t}\n\n\t\t// Fetch games from Steam API\n\t\tconst { games, count } = await fetchSteamGames(apiKey, steamId);\n\n\t\treturn jsonResponse({ games, count });\n\t} catch (error) {\n\t\tconsole.error(\"Steam API Error:\", error);\n\t\treturn jsonResponse(\n\t\t\t{ error: error.message || \"Internal Server Error\" },\n\t\t\t{ status: 500 },\n\t\t);\n\t}\n}\n\n/**\n * Fetches games from Steam Web API\n */\nasync function fetchSteamGames(apiKey, steamId) {\n\t// Build Steam API URL\n\tconst url = new URL(\n\t\t\"https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/\",\n\t);\n\turl.searchParams.set(\"key\", apiKey);\n\turl.searchParams.set(\"steamid\", steamId);\n\turl.searchParams.set(\"include_appinfo\", \"true\");\n\turl.searchParams.set(\"include_played_free_games\", \"true\");\n\n\t// Call Steam API\n\tconst response = await fetch(url);\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Steam API Error: ${response.status} ${response.statusText}`,\n\t\t);\n\t}\n\n\tconst data = await response.json();\n\tconst rawGames = data.response?.games ?? [];\n\n\t// Format games\n\tconst games = rawGames.map((game) => ({\n\t\tid: `steam-${game.appid}`,\n\t\ttitle: game.name,\n\t\tsource: \"steam\",\n\t\tsourceId: String(game.appid),\n\t\tplatform: \"PC\",\n\t\tlastPlayed: game.rtime_last_played\n\t\t\t? new Date(game.rtime_last_played * 1000).toISOString().slice(0, 10)\n\t\t\t: null,\n\t\tplaytimeHours: Math.round((game.playtime_forever / 60) * 10) / 10,\n\t\turl: `https://store.steampowered.com/app/${game.appid}`,\n\t}));\n\n\treturn {\n\t\tgames,\n\t\tcount: games.length,\n\t};\n}\n\n/**\n * CORS preflight response\n */\nfunction handleCORS() {\n\treturn new Response(null, {\n\t\tstatus: 204,\n\t\theaders: getCORSHeaders(),\n\t});\n}\n\n/**\n * JSON response with CORS headers\n */\nfunction jsonResponse(data, options = {}) {\n\treturn new Response(JSON.stringify(data), {\n\t\t...options,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t...getCORSHeaders(),\n\t\t\t...options.headers,\n\t\t},\n\t});\n}\n\n/**\n * Get CORS headers for GitHub Pages\n */\nfunction getCORSHeaders() {\n\treturn {\n\t\t\"Access-Control-Allow-Origin\": \"*\", // Allow all origins (user's own worker)\n\t\t\"Access-Control-Allow-Methods\": \"GET, POST, OPTIONS\",\n\t\t\"Access-Control-Allow-Headers\": \"Content-Type\",\n\t\t\"Access-Control-Max-Age\": \"86400\", // 24 hours\n\t};\n}\n", "import type { Middleware } from \"./common\";\n\nconst drainBody: Middleware = async (request, env, _ctx, middlewareCtx) => {\n\ttry {\n\t\treturn await middlewareCtx.next(request, env);\n\t} finally {\n\t\ttry {\n\t\t\tif (request.body !== null && !request.bodyUsed) {\n\t\t\t\tconst reader = request.body.getReader();\n\t\t\t\twhile (!(await reader.read()).done) {}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tconsole.error(\"Failed to drain the unused request body.\", e);\n\t\t}\n\t}\n};\n\nexport default drainBody;\n", "import type { Middleware } from \"./common\";\n\ninterface JsonError {\n\tmessage?: string;\n\tname?: string;\n\tstack?: string;\n\tcause?: JsonError;\n}\n\nfunction reduceError(e: any): JsonError {\n\treturn {\n\t\tname: e?.name,\n\t\tmessage: e?.message ?? String(e),\n\t\tstack: e?.stack,\n\t\tcause: e?.cause === undefined ? undefined : reduceError(e.cause),\n\t};\n}\n\n// See comment in `bundle.ts` for details on why this is needed\nconst jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => {\n\ttry {\n\t\treturn await middlewareCtx.next(request, env);\n\t} catch (e: any) {\n\t\tconst error = reduceError(e);\n\t\treturn Response.json(error, {\n\t\t\tstatus: 500,\n\t\t\theaders: { \"MF-Experimental-Error-Stack\": \"true\" },\n\t\t});\n\t}\n};\n\nexport default jsonError;\n", "\t\t\t\timport worker, * as OTHER_EXPORTS from \"/Users/felixfoertsch/Developer/whattoplay/workers/steam-proxy.js\";\n\t\t\t\timport * as __MIDDLEWARE_0__ from \"/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts\";\nimport * as __MIDDLEWARE_1__ from \"/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts\";\n\n\t\t\t\texport * from \"/Users/felixfoertsch/Developer/whattoplay/workers/steam-proxy.js\";\n\t\t\t\tconst MIDDLEWARE_TEST_INJECT = \"__INJECT_FOR_TESTING_WRANGLER_MIDDLEWARE__\";\n\t\t\t\texport const __INTERNAL_WRANGLER_MIDDLEWARE__ = [\n\t\t\t\t\t\n\t\t\t\t\t__MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default\n\t\t\t\t]\n\t\t\t\texport default worker;", "export type Awaitable = T | Promise;\n// TODO: allow dispatching more events?\nexport type Dispatcher = (\n\ttype: \"scheduled\",\n\tinit: { cron?: string }\n) => Awaitable;\n\nexport type IncomingRequest = Request<\n\tunknown,\n\tIncomingRequestCfProperties\n>;\n\nexport interface MiddlewareContext {\n\tdispatch: Dispatcher;\n\tnext(request: IncomingRequest, env: any): Awaitable;\n}\n\nexport type Middleware = (\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tmiddlewareCtx: MiddlewareContext\n) => Awaitable;\n\nconst __facade_middleware__: Middleware[] = [];\n\n// The register functions allow for the insertion of one or many middleware,\n// We register internal middleware first in the stack, but have no way of controlling\n// the order that addMiddleware is run in service workers so need an internal function.\nexport function __facade_register__(...args: (Middleware | Middleware[])[]) {\n\t__facade_middleware__.push(...args.flat());\n}\nexport function __facade_registerInternal__(\n\t...args: (Middleware | Middleware[])[]\n) {\n\t__facade_middleware__.unshift(...args.flat());\n}\n\nfunction __facade_invokeChain__(\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tdispatch: Dispatcher,\n\tmiddlewareChain: Middleware[]\n): Awaitable {\n\tconst [head, ...tail] = middlewareChain;\n\tconst middlewareCtx: MiddlewareContext = {\n\t\tdispatch,\n\t\tnext(newRequest, newEnv) {\n\t\t\treturn __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail);\n\t\t},\n\t};\n\treturn head(request, env, ctx, middlewareCtx);\n}\n\nexport function __facade_invoke__(\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tdispatch: Dispatcher,\n\tfinalMiddleware: Middleware\n): Awaitable {\n\treturn __facade_invokeChain__(request, env, ctx, dispatch, [\n\t\t...__facade_middleware__,\n\t\tfinalMiddleware,\n\t]);\n}\n", "// This loads all middlewares exposed on the middleware object and then starts\n// the invocation chain. The big idea is that we can add these to the middleware\n// export dynamically through wrangler, or we can potentially let users directly\n// add them as a sort of \"plugin\" system.\n\nimport ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from \"/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js\";\nimport { __facade_invoke__, __facade_register__, Dispatcher } from \"/Users/felixfoertsch/Developer/whattoplay/node_modules/wrangler/templates/middleware/common.ts\";\nimport type { WorkerEntrypointConstructor } from \"/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js\";\n\n// Preserve all the exports from the worker\nexport * from \"/Users/felixfoertsch/Developer/whattoplay/workers/.wrangler/tmp/bundle-QI6oiq/middleware-insertion-facade.js\";\n\nclass __Facade_ScheduledController__ implements ScheduledController {\n\treadonly #noRetry: ScheduledController[\"noRetry\"];\n\n\tconstructor(\n\t\treadonly scheduledTime: number,\n\t\treadonly cron: string,\n\t\tnoRetry: ScheduledController[\"noRetry\"]\n\t) {\n\t\tthis.#noRetry = noRetry;\n\t}\n\n\tnoRetry() {\n\t\tif (!(this instanceof __Facade_ScheduledController__)) {\n\t\t\tthrow new TypeError(\"Illegal invocation\");\n\t\t}\n\t\t// Need to call native method immediately in case uncaught error thrown\n\t\tthis.#noRetry();\n\t}\n}\n\nfunction wrapExportedHandler(worker: ExportedHandler): ExportedHandler {\n\t// If we don't have any middleware defined, just return the handler as is\n\tif (\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0\n\t) {\n\t\treturn worker;\n\t}\n\t// Otherwise, register all middleware once\n\tfor (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {\n\t\t__facade_register__(middleware);\n\t}\n\n\tconst fetchDispatcher: ExportedHandlerFetchHandler = function (\n\t\trequest,\n\t\tenv,\n\t\tctx\n\t) {\n\t\tif (worker.fetch === undefined) {\n\t\t\tthrow new Error(\"Handler does not export a fetch() function.\");\n\t\t}\n\t\treturn worker.fetch(request, env, ctx);\n\t};\n\n\treturn {\n\t\t...worker,\n\t\tfetch(request, env, ctx) {\n\t\t\tconst dispatcher: Dispatcher = function (type, init) {\n\t\t\t\tif (type === \"scheduled\" && worker.scheduled !== undefined) {\n\t\t\t\t\tconst controller = new __Facade_ScheduledController__(\n\t\t\t\t\t\tDate.now(),\n\t\t\t\t\t\tinit.cron ?? \"\",\n\t\t\t\t\t\t() => {}\n\t\t\t\t\t);\n\t\t\t\t\treturn worker.scheduled(controller, env, ctx);\n\t\t\t\t}\n\t\t\t};\n\t\t\treturn __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);\n\t\t},\n\t};\n}\n\nfunction wrapWorkerEntrypoint(\n\tklass: WorkerEntrypointConstructor\n): WorkerEntrypointConstructor {\n\t// If we don't have any middleware defined, just return the handler as is\n\tif (\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0\n\t) {\n\t\treturn klass;\n\t}\n\t// Otherwise, register all middleware once\n\tfor (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {\n\t\t__facade_register__(middleware);\n\t}\n\n\t// `extend`ing `klass` here so other RPC methods remain callable\n\treturn class extends klass {\n\t\t#fetchDispatcher: ExportedHandlerFetchHandler> = (\n\t\t\trequest,\n\t\t\tenv,\n\t\t\tctx\n\t\t) => {\n\t\t\tthis.env = env;\n\t\t\tthis.ctx = ctx;\n\t\t\tif (super.fetch === undefined) {\n\t\t\t\tthrow new Error(\"Entrypoint class does not define a fetch() function.\");\n\t\t\t}\n\t\t\treturn super.fetch(request);\n\t\t};\n\n\t\t#dispatcher: Dispatcher = (type, init) => {\n\t\t\tif (type === \"scheduled\" && super.scheduled !== undefined) {\n\t\t\t\tconst controller = new __Facade_ScheduledController__(\n\t\t\t\t\tDate.now(),\n\t\t\t\t\tinit.cron ?? \"\",\n\t\t\t\t\t() => {}\n\t\t\t\t);\n\t\t\t\treturn super.scheduled(controller);\n\t\t\t}\n\t\t};\n\n\t\tfetch(request: Request) {\n\t\t\treturn __facade_invoke__(\n\t\t\t\trequest,\n\t\t\t\tthis.env,\n\t\t\t\tthis.ctx,\n\t\t\t\tthis.#dispatcher,\n\t\t\t\tthis.#fetchDispatcher\n\t\t\t);\n\t\t}\n\t};\n}\n\nlet WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;\nif (typeof ENTRY === \"object\") {\n\tWRAPPED_ENTRY = wrapExportedHandler(ENTRY);\n} else if (typeof ENTRY === \"function\") {\n\tWRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);\n}\nexport default WRAPPED_ENTRY;\n"], - "mappings": "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", - "names": [] -} diff --git a/workers/steam-proxy.js b/workers/steam-proxy.js deleted file mode 100644 index db85f81..0000000 --- a/workers/steam-proxy.js +++ /dev/null @@ -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 - }; -} diff --git a/workers/wrangler.toml b/workers/wrangler.toml deleted file mode 100644 index 03a410a..0000000 --- a/workers/wrangler.toml +++ /dev/null @@ -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