diff --git a/.github/workflows/cloudflare-deploy.yml b/.github/workflows/cloudflare-deploy.yml new file mode 100644 index 0000000..544b888 --- /dev/null +++ b/.github/workflows/cloudflare-deploy.yml @@ -0,0 +1,125 @@ +name: Deploy to Cloudflare + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install Dependencies + run: | + npm ci + cd dropship-frontend && npm ci + cd ../dropship-backend && npm ci + npm install -g wrangler + + - name: Build Frontend + run: | + cd dropship-frontend + npm run build + env: + VITE_API_URL: ${{ secrets.CLOUDFLARE_API_URL }} + VITE_WS_URL: ${{ secrets.CLOUDFLARE_WS_URL }} + + - name: Deploy Frontend to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages publish dropship-frontend/build --project-name=dropship-platform + + - name: Deploy Worker + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: publish cloudflare/worker.js + env: + API_ENDPOINT: ${{ secrets.API_ENDPOINT }} + WS_ENDPOINT: ${{ secrets.WS_ENDPOINT }} + CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + + - name: Create KV Namespace (if not exists) + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: kv:namespace create DROPSHIP_KV || true + + - name: Configure DNS + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: | + dns record create \ + --type CNAME \ + --name ${{ secrets.DOMAIN }} \ + --content ${{ secrets.CLOUDFLARE_PAGES_URL }} \ + --proxied true + + - name: Configure Workers Routes + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: | + route add ${{ secrets.DOMAIN }}/* dropship-platform + route add ${{ secrets.DOMAIN }}/api/* dropship-platform-api + route add ${{ secrets.DOMAIN }}/ws dropship-platform-ws + + - name: Deploy D1 Database + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: d1 create dropship-platform-db --location=WEUR + + - name: Configure Cron Triggers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: | + cron create "*/5 * * * *" dropship-platform-cron + cron enable dropship-platform-cron + + - name: Deploy R2 Storage + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: r2 bucket create dropship-platform-storage + + - name: Configure Analytics + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: analytics enable + + - name: Notify Deployment Status + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + +env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/CLOUDFLARE_DEPLOYMENT.md b/CLOUDFLARE_DEPLOYMENT.md new file mode 100644 index 0000000..8ddb812 --- /dev/null +++ b/CLOUDFLARE_DEPLOYMENT.md @@ -0,0 +1,261 @@ +# Cloudflare Deployment Guide + +This guide explains how to deploy the Dropship Platform to Cloudflare using Pages, Workers, and other Cloudflare services. + +## Prerequisites + +1. Cloudflare Account +2. Domain registered with Cloudflare +3. Node.js 18 or later +4. Wrangler CLI (`npm install -g wrangler`) +5. GitHub account + +## Setup Steps + +### 1. Cloudflare Account Setup + +1. Log in to your Cloudflare dashboard +2. Note down your Account ID from the dashboard +3. Create an API token with the following permissions: + - Account Settings: Read + - Workers Scripts: Edit + - Workers Routes: Edit + - Workers KV: Edit + - DNS: Edit + - Pages: Edit + +### 2. GitHub Repository Setup + +1. Add the following secrets to your GitHub repository: +```bash +CLOUDFLARE_API_TOKEN=your_api_token +CLOUDFLARE_ACCOUNT_ID=your_account_id +CLOUDFLARE_API_URL=https://api.your-domain.com +CLOUDFLARE_WS_URL=wss://api.your-domain.com +CLOUDFLARE_PAGES_URL=your-project.pages.dev +DOMAIN=your-domain.com +API_ENDPOINT=https://api.your-domain.com +WS_ENDPOINT=wss://api.your-domain.com +SLACK_WEBHOOK_URL=your_slack_webhook_url +``` + +### 3. Local Development Setup + +1. Install Wrangler CLI: +```bash +npm install -g wrangler +``` + +2. Login to Cloudflare: +```bash +wrangler login +``` + +3. Update wrangler.toml with your account details: +```toml +account_id = "your_account_id" +zone_id = "your_zone_id" +route = "your-domain.com/*" +``` + +### 4. Cloudflare Services Configuration + +#### Pages Setup +1. Create a new Pages project: +```bash +wrangler pages project create dropship-platform +``` + +2. Configure build settings: + - Build command: `npm run build` + - Build output directory: `build` + - Root directory: `dropship-frontend` + +#### Workers Setup +1. Create KV namespace: +```bash +wrangler kv:namespace create DROPSHIP_KV +``` + +2. Create D1 database: +```bash +wrangler d1 create dropship-platform-db +``` + +3. Create R2 bucket: +```bash +wrangler r2 bucket create dropship-platform-storage +``` + +#### DNS Setup +1. Add DNS records: +```bash +wrangler dns record create --type CNAME --name @ --content your-project.pages.dev --proxied true +``` + +### 5. Deployment + +1. Push changes to the main branch to trigger automatic deployment: +```bash +git add . +git commit -m "Update deployment configuration" +git push origin main +``` + +2. Monitor deployment in GitHub Actions: + - Go to your repository's Actions tab + - Check the "Deploy to Cloudflare" workflow + +### 6. Post-Deployment Verification + +1. Check Pages deployment: + - Visit your domain + - Verify all pages load correctly + - Test navigation and features + +2. Verify Worker functionality: + - Test API endpoints + - Check WebSocket connections + - Verify caching behavior + +3. Monitor performance: + - Check Cloudflare Analytics + - Review Worker metrics + - Monitor error rates + +## Directory Structure + +``` +. +├── cloudflare/ +│ └── worker.js # Cloudflare Worker script +├── wrangler.toml # Wrangler configuration +└── .github/ + └── workflows/ + └── cloudflare-deploy.yml # Deployment workflow +``` + +## Environment Variables + +### Frontend (.env) +```env +VITE_API_URL=https://api.your-domain.com +VITE_WS_URL=wss://api.your-domain.com +``` + +### Worker (Environment Variables in Cloudflare Dashboard) +```env +API_ENDPOINT=https://api.your-domain.com +WS_ENDPOINT=wss://api.your-domain.com +CF_API_TOKEN=your_cloudflare_api_token +``` + +## Monitoring and Maintenance + +### Analytics +1. Enable Cloudflare Analytics: +```bash +wrangler analytics enable +``` + +2. Monitor in Cloudflare Dashboard: + - Request statistics + - Error rates + - Performance metrics + +### Logs +1. View Worker logs: +```bash +wrangler tail +``` + +2. Check deployment logs in GitHub Actions + +### Scheduled Tasks +1. View scheduled tasks: +```bash +wrangler cron list +``` + +2. Monitor task execution in Worker logs + +## Troubleshooting + +### Common Issues + +1. **Deployment Failures** + - Check GitHub Actions logs + - Verify environment variables + - Ensure Cloudflare API token permissions + +2. **Worker Errors** + - Check Worker logs: `wrangler tail` + - Verify KV namespace bindings + - Check route configurations + +3. **Pages Build Failures** + - Review build logs in GitHub Actions + - Check build command and output directory + - Verify dependencies are installed + +### Debug Mode + +Enable debug mode in worker.js: +```javascript +const DEBUG = true; +``` + +This will provide detailed error messages in responses. + +## Scaling + +### KV Storage +- Monitor KV usage in Cloudflare Dashboard +- Implement caching strategies +- Use bulk operations for large datasets + +### Worker Performance +- Optimize API routes +- Implement efficient caching +- Use Web Workers for CPU-intensive tasks + +### R2 Storage +- Monitor storage usage +- Implement lifecycle policies +- Use signed URLs for secure access + +## Security + +### Headers +- CORS configuration in worker.js +- Security headers in nginx.conf +- CSP configuration + +### Authentication +- JWT validation in Worker +- Rate limiting +- IP filtering + +### Data Protection +- Enable encryption at rest +- Implement backup strategies +- Monitor access logs + +## Rollback Procedure + +1. Revert to previous version: +```bash +wrangler rollback +``` + +2. Verify DNS records: +```bash +wrangler dns record list +``` + +3. Check Worker routes: +```bash +wrangler route list +``` + +For more detailed information about specific deployment scenarios or troubleshooting, please refer to the [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/). diff --git a/cloudflare/worker.js b/cloudflare/worker.js new file mode 100644 index 0000000..3ce7937 --- /dev/null +++ b/cloudflare/worker.js @@ -0,0 +1,240 @@ +import { getAssetFromKV } from '@cloudflare/kv-asset-handler'; +import manifestJSON from '__STATIC_CONTENT_MANIFEST'; + +const assetManifest = JSON.parse(manifestJSON); +const DEBUG = false; + +async function handleEvent(event) { + const url = new URL(event.request.url); + + try { + // Handle API requests + if (url.pathname.startsWith('/api')) { + return await handleApiRequest(event); + } + + // Handle static assets and frontend routes + return await getAssetFromKV(event, { + mapRequestToAsset: req => { + const url = new URL(req.url); + + // SPA fallback - serve index.html for all non-file requests + if (!url.pathname.includes('.')) { + return new Request(`${url.origin}/index.html`, req); + } + return req; + }, + cacheControl: { + browserTTL: 60 * 60 * 24, // 1 day + edgeTTL: 60 * 60 * 24 * 365, // 1 year + bypassCache: false, + }, + }); + } catch (e) { + if (DEBUG) { + return new Response(e.message || e.toString(), { + status: 500, + }); + } + return new Response('Internal Error', { status: 500 }); + } +} + +async function handleApiRequest(event) { + const url = new URL(event.request.url); + const apiPath = url.pathname.replace('/api', ''); + const apiUrl = `${API_ENDPOINT}${apiPath}`; + + // Forward the request to the backend API + const request = new Request(apiUrl, { + method: event.request.method, + headers: { + 'Content-Type': 'application/json', + ...event.request.headers, + }, + body: event.request.body, + }); + + try { + const response = await fetch(request); + + // Handle CORS + const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }; + + if (event.request.method === 'OPTIONS') { + return new Response(null, { + headers: corsHeaders, + }); + } + + // Clone the response and add CORS headers + const modifiedResponse = new Response(response.body, response); + Object.keys(corsHeaders).forEach(key => { + modifiedResponse.headers.set(key, corsHeaders[key]); + }); + + return modifiedResponse; + } catch (error) { + if (DEBUG) { + return new Response(error.message || error.toString(), { + status: 500, + }); + } + return new Response('API Error', { status: 500 }); + } +} + +// Cache API responses +const apiCache = caches.default; +const API_CACHE_TIME = 60 * 5; // 5 minutes + +async function handleCachedApiRequest(event) { + const cache = await apiCache.match(event.request); + + if (cache) { + return cache; + } + + const response = await handleApiRequest(event); + + if (response.ok) { + event.waitUntil( + apiCache.put( + event.request, + response.clone(), + { expirationTtl: API_CACHE_TIME } + ) + ); + } + + return response; +} + +// WebSocket proxy for real-time features +async function handleWebSocket(event) { + const upgradeHeader = event.request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + return new Response('Expected Upgrade: websocket', { status: 426 }); + } + + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + server.accept(); + + // Forward WebSocket messages to backend + const backendWs = new WebSocket(WS_ENDPOINT); + + server.addEventListener('message', event => { + if (backendWs.readyState === WebSocket.OPEN) { + backendWs.send(event.data); + } + }); + + backendWs.addEventListener('message', event => { + if (server.readyState === WebSocket.OPEN) { + server.send(event.data); + } + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); +} + +addEventListener('fetch', event => { + try { + const url = new URL(event.request.url); + + // Handle WebSocket connections + if (url.pathname === '/ws') { + event.respondWith(handleWebSocket(event)); + return; + } + + // Handle API requests with caching + if (url.pathname.startsWith('/api')) { + event.respondWith(handleCachedApiRequest(event)); + return; + } + + // Handle all other requests + event.respondWith(handleEvent(event)); + } catch (e) { + if (DEBUG) { + event.respondWith( + new Response(e.message || e.toString(), { status: 500 }) + ); + } else { + event.respondWith( + new Response('Internal Error', { status: 500 }) + ); + } + } +}); + +// Handle scheduled tasks +addEventListener('scheduled', event => { + event.waitUntil(handleScheduledTask(event)); +}); + +async function handleScheduledTask(event) { + try { + await Promise.all([ + cleanupExpiredCache(), + syncData(), + generateReports(), + sendNotifications(), + ]); + } catch (error) { + if (DEBUG) { + console.error('Scheduled task error:', error); + } + } +} + +// Analytics and monitoring +addEventListener('fetch', event => { + const start = Date.now(); + + event.waitUntil( + event.respondWith(handleEvent(event)) + .then(response => { + const duration = Date.now() - start; + + // Log metrics to Cloudflare Analytics + event.waitUntil( + fetch('https://api.cloudflare.com/client/v4/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CF_API_TOKEN}`, + }, + body: JSON.stringify({ + query: ` + mutation ($duration: Int!, $status: Int!, $path: String!) { + createRequestMetric( + duration: $duration, + status: $status, + path: $path + ) + } + `, + variables: { + duration, + status: response.status, + path: new URL(event.request.url).pathname, + }, + }), + }) + ); + + return response; + }) + ); +}); diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..e69de29 diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..d087d87 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,33 @@ +name = "dropship-platform" +type = "webpack" +account_id = "" # Your Cloudflare account ID +workers_dev = true +route = "" # Your custom domain route, e.g., "example.com/*" +zone_id = "" # Your Cloudflare zone ID + +[site] +bucket = "./dropship-frontend/build" +entry-point = "workers-site" + +[env.production] +name = "dropship-platform-prod" +route = "" # Production domain route +zone_id = "" # Production zone ID + +[env.staging] +name = "dropship-platform-staging" +workers_dev = true + +[build] +command = "npm run build" +upload.format = "service-worker" + +[build.upload] +dir = "dist" +format = "modules" +main = "./worker.js" + +[[kv_namespaces]] +binding = "DROPSHIP_KV" +id = "" # Your KV namespace ID +preview_id = "" # Preview KV namespace ID