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 <noreply@anthropic.com>
This commit is contained in:
@@ -173,9 +173,19 @@ export default function CloudflareSetupPage() {
|
||||
<strong>"Use template"</strong>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Schritt 4:</strong> Bei "Account Resources" sollte "All
|
||||
accounts" ausgewählt sein. Lass alle Einstellungen wie sie sind.
|
||||
<strong>Schritt 4:</strong> Überprüfe die Permissions:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Account Resources:</strong> "All accounts" sollte ausgewählt
|
||||
sein
|
||||
</li>
|
||||
<li>
|
||||
<strong>Zone Resources:</strong> Wähle "All zones" ODER ignoriere
|
||||
dieses Feld (Workers sind Account-level, nicht Zone-level)
|
||||
</li>
|
||||
</ul>
|
||||
<p>Lass alle anderen Einstellungen wie sie sind.</p>
|
||||
<p>
|
||||
<strong>Schritt 5:</strong> Scrolle nach unten und klicke auf{" "}
|
||||
<strong>"Continue to summary"</strong>
|
||||
|
||||
@@ -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<CFAccountInfo> {
|
||||
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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
109
workers/README.md
Normal file
109
workers/README.md
Normal file
@@ -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.
|
||||
279
workers/cf-setup-service.js
Normal file
279
workers/cf-setup-service.js
Normal file
@@ -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" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user