From 985c61ed74044126b82b043a674d6dbab31e96dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Fri, 6 Feb 2026 20:44:53 +0100 Subject: [PATCH] add Cloudflare Setup Service to bypass CORS restrictions The Cloudflare API blocks direct browser requests due to CORS. This setup service acts as a proxy, allowing browser-based worker deployment. Changes: - Create cf-setup-service.js worker for CORS proxy - Update CloudflareService to use setup service API - Add deployment instructions in workers/README.md - Improve token creation guide with Zone field explanation Next steps: 1. Deploy setup service: npx wrangler deploy workers/cf-setup-service.js --name whattoplay-setup 2. Update CF_SETUP_SERVICE_URL in CloudflareService.ts with your URL 3. Commit and push Co-Authored-By: Claude --- src/pages/Settings/CloudflareSetupPage.tsx | 14 +- src/services/CloudflareService.ts | 177 ++++--------- workers/README.md | 109 ++++++++ workers/cf-setup-service.js | 279 +++++++++++++++++++++ 4 files changed, 453 insertions(+), 126 deletions(-) create mode 100644 workers/README.md create mode 100644 workers/cf-setup-service.js diff --git a/src/pages/Settings/CloudflareSetupPage.tsx b/src/pages/Settings/CloudflareSetupPage.tsx index aa79a36..2917248 100644 --- a/src/pages/Settings/CloudflareSetupPage.tsx +++ b/src/pages/Settings/CloudflareSetupPage.tsx @@ -173,9 +173,19 @@ export default function CloudflareSetupPage() { "Use template"

- Schritt 4: Bei "Account Resources" sollte "All - accounts" ausgewählt sein. Lass alle Einstellungen wie sie sind. + Schritt 4: Überprüfe die Permissions:

+ +

Lass alle anderen Einstellungen wie sie sind.

Schritt 5: Scrolle nach unten und klicke auf{" "} "Continue to summary" diff --git a/src/services/CloudflareService.ts b/src/services/CloudflareService.ts index 0377c20..8496131 100644 --- a/src/services/CloudflareService.ts +++ b/src/services/CloudflareService.ts @@ -1,9 +1,15 @@ /** * CloudflareService - API Wrapper für Cloudflare Workers Deployment * Ermöglicht automatisches Worker Deployment direkt aus der PWA + * + * Uses a CORS proxy service to bypass browser restrictions when calling + * the Cloudflare API. You can deploy your own setup service using + * workers/cf-setup-service.js */ -const CF_API_BASE = "https://api.cloudflare.com/client/v4"; +// Default setup service URL - you can deploy your own instance +const CF_SETUP_SERVICE_URL = + "https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev"; export interface CFAccountInfo { valid: boolean; @@ -25,64 +31,41 @@ export class CloudflareService { */ static async validateToken(apiToken: string): Promise { try { - // Get accounts - const accountsResponse = await fetch(`${CF_API_BASE}/accounts`, { + const response = await fetch(CF_SETUP_SERVICE_URL, { + method: "POST", headers: { - Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", }, + body: JSON.stringify({ + action: "validate", + apiToken, + }), }); - if (!accountsResponse.ok) { + if (!response.ok) { return { valid: false, - error: `API Error: ${accountsResponse.status} ${accountsResponse.statusText}`, + error: `Setup service error: ${response.status} ${response.statusText}`, }; } - const accountsData = await accountsResponse.json(); - - if ( - !accountsData.success || - !accountsData.result || - accountsData.result.length === 0 - ) { - return { - valid: false, - error: "No Cloudflare accounts found for this token", - }; - } - - const account = accountsData.result[0]; - - // Get Workers subdomain - const subdomainResponse = await fetch( - `${CF_API_BASE}/accounts/${account.id}/workers/subdomain`, - { - headers: { - Authorization: `Bearer ${apiToken}`, - }, - }, - ); - - let subdomain: string | undefined; - - if (subdomainResponse.ok) { - const subdomainData = await subdomainResponse.json(); - subdomain = subdomainData.result?.subdomain || undefined; - } + const data = await response.json(); return { - valid: true, - accountId: account.id, - accountName: account.name, - subdomain, + valid: data.valid, + accountId: data.accountId, + accountName: data.accountName, + subdomain: data.subdomain, + error: data.error, }; } catch (error) { console.error("Token validation failed:", error); return { valid: false, error: - error instanceof Error ? error.message : "Token validation failed", + error instanceof Error + ? error.message + : "Failed to connect to setup service", }; } } @@ -108,52 +91,38 @@ export class CloudflareService { const scriptContent = await scriptResponse.text(); - // Prepare multipart form data - const formData = new FormData(); - - formData.append( - "metadata", - JSON.stringify({ - main_module: "steam-proxy.js", - compatibility_date: "2024-01-01", - bindings: [], - }), - ); - - formData.append( - "steam-proxy.js", - new Blob([scriptContent], { - type: "application/javascript+module", - }), - "steam-proxy.js", - ); - - // Deploy Worker - const deployResponse = await fetch( - `${CF_API_BASE}/accounts/${accountId}/workers/scripts/${scriptName}`, - { - method: "PUT", - headers: { - Authorization: `Bearer ${apiToken}`, - }, - body: formData, + // Deploy via setup service + const response = await fetch(CF_SETUP_SERVICE_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ + action: "deploy", + apiToken, + accountId, + scriptName, + scriptContent, + }), + }); - const deployData = await deployResponse.json(); - - if (!deployData.success) { - const errorMsg = deployData.errors?.[0]?.message || "Deployment failed"; + if (!response.ok) { return { success: false, - error: errorMsg, + error: `Setup service error: ${response.status} ${response.statusText}`, }; } - // Get subdomain for Worker URL - const accountInfo = await this.validateToken(apiToken); + const data = await response.json(); - if (!accountInfo.subdomain) { + if (!data.success) { + return { + success: false, + error: data.error || "Deployment failed", + }; + } + + if (!data.workerUrl) { return { success: false, error: @@ -161,58 +130,18 @@ export class CloudflareService { }; } - const workerUrl = `https://${scriptName}.${accountInfo.subdomain}.workers.dev`; - return { success: true, - workerUrl, + workerUrl: data.workerUrl, }; } catch (error) { console.error("Worker deployment failed:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Deployment failed", - }; - } - } - - /** - * Enables workers.dev subdomain (if not already enabled) - */ - static async enableSubdomain( - apiToken: string, - accountId: string, - subdomain: string, - ): Promise<{ success: boolean; error?: string }> { - try { - const response = await fetch( - `${CF_API_BASE}/accounts/${accountId}/workers/subdomain`, - { - method: "PUT", - headers: { - Authorization: `Bearer ${apiToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ subdomain }), - }, - ); - - const data = await response.json(); - - if (!data.success) { - return { - success: false, - error: data.errors?.[0]?.message || "Failed to enable subdomain", - }; - } - - return { success: true }; - } catch (error) { - console.error("Failed to enable subdomain:", error); return { success: false, error: - error instanceof Error ? error.message : "Failed to enable subdomain", + error instanceof Error + ? error.message + : "Failed to connect to setup service", }; } } diff --git a/workers/README.md b/workers/README.md new file mode 100644 index 0000000..4fe91da --- /dev/null +++ b/workers/README.md @@ -0,0 +1,109 @@ +# Cloudflare Setup Service + +This is a CORS proxy service that allows browser-based deployment of Cloudflare Workers. Deploy this once on your Cloudflare account, and all users can use it to deploy their own workers without CORS restrictions. + +## Why is this needed? + +The Cloudflare API doesn't allow direct browser requests due to CORS restrictions. This service acts as a proxy, forwarding requests from the browser to the Cloudflare API. + +## Security + +- **API tokens are NOT stored or logged** - they are only used to validate and deploy workers +- All communication happens over HTTPS +- The service only accepts POST requests with specific actions +- CORS is enabled to allow browser access + +## Deployment + +### Option 1: Quick Deploy with Wrangler + +```bash +cd workers +npx wrangler deploy cf-setup-service.js --name whattoplay-setup +``` + +This will deploy the service and give you a URL like: +`https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev` + +### Option 2: Deploy via Dashboard + +1. Go to https://dash.cloudflare.com +2. Navigate to Workers & Pages +3. Click "Create Application" → "Create Worker" +4. Name it `whattoplay-setup` +5. Click "Deploy" +6. Click "Edit Code" +7. Replace the default code with the contents of `cf-setup-service.js` +8. Click "Save and Deploy" + +## Configuration + +After deployment, update the service URL in your app: + +1. Open `src/services/CloudflareService.ts` +2. Replace `YOUR-SUBDOMAIN` with your actual subdomain: + ```typescript + const CF_SETUP_SERVICE_URL = + "https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev"; + ``` +3. Commit and deploy your app + +## API + +### POST / + +**Validate Token:** + +```json +{ + "action": "validate", + "apiToken": "your-cf-api-token" +} +``` + +Response: + +```json +{ + "valid": true, + "accountId": "...", + "accountName": "...", + "subdomain": "..." +} +``` + +**Deploy Worker:** + +```json +{ + "action": "deploy", + "apiToken": "your-cf-api-token", + "accountId": "your-account-id", + "scriptName": "worker-name", + "scriptContent": "export default { async fetch(...) { ... } }" +} +``` + +Response: + +```json +{ + "success": true, + "workerUrl": "https://worker-name.your-subdomain.workers.dev" +} +``` + +## Usage Limits + +Cloudflare Workers Free Tier: + +- 100,000 requests/day +- 10ms CPU time per request + +This should be more than enough for personal use. If you expect heavy usage, consider upgrading to the Workers Paid plan ($5/month for 10M requests). + +## Sharing + +You can share this service URL with other users of your app. They will use your deployment to deploy their own workers on their own Cloudflare accounts. + +**Note:** This service does NOT store any data. Each user's worker is deployed on their own Cloudflare account, using their own API token. diff --git a/workers/cf-setup-service.js b/workers/cf-setup-service.js new file mode 100644 index 0000000..e5c5a46 --- /dev/null +++ b/workers/cf-setup-service.js @@ -0,0 +1,279 @@ +/** + * Cloudflare Worker Setup Service + * + * This worker acts as a CORS proxy for the Cloudflare API, allowing + * browser-based deployment of Workers without exposing API tokens. + * + * Deploy this once on your Cloudflare account, then share the URL. + * All users can use this service to deploy their own workers. + * + * SECURITY: Tokens are NOT stored, only validated and used for deployment. + */ + +export default { + async fetch(request, env, ctx) { + // Enable CORS + const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; + + // Handle CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + // Only allow POST + if (request.method !== "POST") { + return new Response(JSON.stringify({ error: "Method not allowed" }), { + status: 405, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + try { + const { action, apiToken, accountId, scriptName, scriptContent } = + await request.json(); + + // Validate required fields + if (!action || !apiToken) { + return new Response( + JSON.stringify({ error: "Missing required fields" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + // Route to appropriate handler + if (action === "validate") { + return await handleValidateToken(apiToken, corsHeaders); + } else if (action === "deploy") { + if (!accountId || !scriptName || !scriptContent) { + return new Response( + JSON.stringify({ error: "Missing deployment fields" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + return await handleDeployWorker( + apiToken, + accountId, + scriptName, + scriptContent, + corsHeaders, + ); + } else { + return new Response(JSON.stringify({ error: "Unknown action" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + } catch (error) { + console.error("Setup service error:", error); + return new Response( + JSON.stringify({ + error: "Internal server error", + message: error.message, + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + }, +}; + +/** + * Validates API token and retrieves account info + */ +async function handleValidateToken(apiToken, corsHeaders) { + try { + // Get accounts + const accountsResponse = await fetch( + "https://api.cloudflare.com/client/v4/accounts", + { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }, + ); + + if (!accountsResponse.ok) { + const errorData = await accountsResponse.json(); + return new Response( + JSON.stringify({ + valid: false, + error: + errorData.errors?.[0]?.message || + `API Error: ${accountsResponse.status}`, + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const accountsData = await accountsResponse.json(); + + if ( + !accountsData.success || + !accountsData.result || + accountsData.result.length === 0 + ) { + return new Response( + JSON.stringify({ + valid: false, + error: "No Cloudflare accounts found for this token", + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const account = accountsData.result[0]; + + // Get Workers subdomain + const subdomainResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${account.id}/workers/subdomain`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }, + ); + + let subdomain; + if (subdomainResponse.ok) { + const subdomainData = await subdomainResponse.json(); + subdomain = subdomainData.result?.subdomain || undefined; + } + + return new Response( + JSON.stringify({ + valid: true, + accountId: account.id, + accountName: account.name, + subdomain, + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } catch (error) { + return new Response( + JSON.stringify({ + valid: false, + error: error.message, + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } +} + +/** + * Deploys Worker script to Cloudflare + */ +async function handleDeployWorker( + apiToken, + accountId, + scriptName, + scriptContent, + corsHeaders, +) { + try { + // Prepare multipart form data + const formData = new FormData(); + + formData.append( + "metadata", + JSON.stringify({ + main_module: "script.js", + compatibility_date: "2024-01-01", + bindings: [], + }), + ); + + formData.append( + "script.js", + new Blob([scriptContent], { type: "application/javascript+module" }), + "script.js", + ); + + // Deploy Worker + const deployResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + body: formData, + }, + ); + + const deployData = await deployResponse.json(); + + if (!deployData.success) { + const errorMsg = deployData.errors?.[0]?.message || "Deployment failed"; + return new Response( + JSON.stringify({ + success: false, + error: errorMsg, + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + // Get subdomain for Worker URL + const subdomainResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/subdomain`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }, + ); + + let workerUrl; + if (subdomainResponse.ok) { + const subdomainData = await subdomainResponse.json(); + const subdomain = subdomainData.result?.subdomain; + if (subdomain) { + workerUrl = `https://${scriptName}.${subdomain}.workers.dev`; + } + } + + return new Response( + JSON.stringify({ + success: true, + workerUrl, + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } catch (error) { + return new Response( + JSON.stringify({ + success: false, + error: error.message, + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } +}