/** * Main API Worker for Rocky Mountain Vending * * This worker handles all API endpoints: * - /api/manuals/* - Serve PDF manuals from R2 * - /api/thumbnails/* - Serve thumbnail images from R2 * - /api/sitemap-submit - Submit sitemap to Google Search Console * - /api/request-indexing - Request URL indexing * - /health - Health check endpoint * * Account ID: bd6f76304a840ba11b75f9ced84264f4 * Temp subdomain: matt-bd6.workers.dev */ export interface Env { // R2 bucket bindings MANUALS_BUCKET: R2Bucket THUMBNAILS_BUCKET: R2Bucket // Google Search Console credentials (optional) GOOGLE_SERVICE_ACCOUNT_EMAIL?: string GOOGLE_PRIVATE_KEY?: string GOOGLE_SITE_URL?: string // Site configuration SITE_URL?: string SITE_DOMAIN?: string // CORS configuration ALLOWED_ORIGINS?: string } const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url) const pathname = url.pathname // Handle CORS preflight if (request.method === "OPTIONS") { return new Response(null, { headers: CORS_HEADERS, }) } // Route requests if (pathname.startsWith("/api/manuals/")) { return handleManuals(request, env, url) } if (pathname.startsWith("/api/thumbnails/")) { return handleThumbnails(request, env, url) } if (pathname === "/api/sitemap-submit") { return handleSitemapSubmit(request, env) } if (pathname === "/api/request-indexing") { return handleRequestIndexing(request, env) } if (pathname === "/health") { return handleHealth(env) } return new Response("Not found", { status: 404, headers: CORS_HEADERS, }) }, } /** * Handle manual PDF requests from R2 */ async function handleManuals( request: Request, env: Env, url: URL ): Promise { if (request.method !== "GET") { return new Response("Method not allowed", { status: 405, headers: CORS_HEADERS, }) } // Extract path from /api/manuals/... const pathMatch = url.pathname.match(/^\/api\/manuals\/(.+)$/) if (!pathMatch) { return new Response("Invalid path", { status: 400, headers: CORS_HEADERS, }) } const filePath = decodeURIComponent(pathMatch[1]) // Security: Prevent directory traversal if (filePath.includes("..") || filePath.startsWith("/")) { return new Response("Invalid path", { status: 400, headers: CORS_HEADERS, }) } // Only allow PDF files if (!filePath.toLowerCase().endsWith(".pdf")) { return new Response("Invalid file type", { status: 400, headers: CORS_HEADERS, }) } try { const object = await env.MANUALS_BUCKET.get(filePath) if (!object) { return new Response("File not found", { status: 404, headers: CORS_HEADERS, }) } const filename = filePath.split("/").pop() || "manual.pdf" const headers = new Headers(CORS_HEADERS) headers.set("Content-Type", "application/pdf") headers.set( "Content-Disposition", `inline; filename="${encodeURIComponent(filename)}"` ) headers.set("Cache-Control", "public, max-age=31536000, immutable") headers.set("X-Content-Type-Options", "nosniff") return new Response(object.body, { headers }) } catch (error) { console.error("Error serving manual:", error) return new Response( JSON.stringify({ error: "Failed to serve manual", message: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } } /** * Handle thumbnail image requests from R2 */ async function handleThumbnails( request: Request, env: Env, url: URL ): Promise { if (request.method !== "GET") { return new Response("Method not allowed", { status: 405, headers: CORS_HEADERS, }) } // Extract path from /api/thumbnails/... const pathMatch = url.pathname.match(/^\/api\/thumbnails\/(.+)$/) if (!pathMatch) { return new Response("Invalid path", { status: 400, headers: CORS_HEADERS, }) } const filePath = decodeURIComponent(pathMatch[1]) // Security: Prevent directory traversal if (filePath.includes("..") || filePath.startsWith("/")) { return new Response("Invalid path", { status: 400, headers: CORS_HEADERS, }) } // Only allow image files const lowerPath = filePath.toLowerCase() const allowedExtensions = [".jpg", ".jpeg", ".png", ".webp"] if (!allowedExtensions.some((ext) => lowerPath.endsWith(ext))) { return new Response("Invalid file type", { status: 400, headers: CORS_HEADERS, }) } try { const object = await env.THUMBNAILS_BUCKET.get(filePath) if (!object) { return new Response("Thumbnail not found", { status: 404, headers: CORS_HEADERS, }) } const filename = filePath.split("/").pop() || "thumbnail.jpg" const contentType = lowerPath.endsWith(".png") ? "image/png" : lowerPath.endsWith(".webp") ? "image/webp" : "image/jpeg" const headers = new Headers(CORS_HEADERS) headers.set("Content-Type", contentType) headers.set( "Content-Disposition", `inline; filename="${encodeURIComponent(filename)}"` ) headers.set("Cache-Control", "public, max-age=31536000, immutable") headers.set("X-Content-Type-Options", "nosniff") return new Response(object.body, { headers }) } catch (error) { console.error("Error serving thumbnail:", error) return new Response( JSON.stringify({ error: "Failed to serve thumbnail", message: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } } /** * Handle sitemap submission to Google Search Console */ async function handleSitemapSubmit( request: Request, env: Env ): Promise { if (request.method === "GET") { const siteUrl = env.SITE_URL || env.GOOGLE_SITE_URL || "https://rockymountainvending.com" const sitemapUrl = `${siteUrl}/sitemap.xml` return new Response( JSON.stringify({ message: "Google Search Console Sitemap Submission API", sitemapUrl, configured: !!( env.GOOGLE_SERVICE_ACCOUNT_EMAIL && env.GOOGLE_PRIVATE_KEY ), instructions: "POST to this endpoint with optional sitemapUrl and siteUrl in body", }), { headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } if (request.method !== "POST") { return new Response("Method not allowed", { status: 405, headers: CORS_HEADERS, }) } try { const body = (await request.json()) as { sitemapUrl?: string siteUrl?: string } const siteUrl = body.siteUrl || env.SITE_URL || env.GOOGLE_SITE_URL || "https://rockymountainvending.com" const sitemapUrl = body.sitemapUrl || `${siteUrl}/sitemap.xml` if (!env.GOOGLE_SERVICE_ACCOUNT_EMAIL || !env.GOOGLE_PRIVATE_KEY) { return new Response( JSON.stringify({ success: false, error: "Google Search Console credentials not configured", message: "Please set GOOGLE_SERVICE_ACCOUNT_EMAIL and GOOGLE_PRIVATE_KEY environment variables", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } // Note: Full Google Search Console API integration would require googleapis library // For now, this is a placeholder that returns success // To implement fully, you would need to: // 1. Install googleapis in the worker (may require bundling) // 2. Use JWT authentication with service account // 3. Call searchconsole.sitemaps.submit() return new Response( JSON.stringify({ success: true, message: "Sitemap submission endpoint ready", sitemapUrl, siteUrl, note: "Google Search Console API integration requires additional setup. See documentation.", }), { headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } catch (error) { return new Response( JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Unknown error occurred", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } } /** * Handle URL indexing requests */ async function handleRequestIndexing( request: Request, env: Env ): Promise { if (request.method === "GET") { return new Response( JSON.stringify({ message: "Google Search Console URL Indexing Request API", configured: !!( env.GOOGLE_SERVICE_ACCOUNT_EMAIL && env.GOOGLE_PRIVATE_KEY ), instructions: "POST to this endpoint with { url: 'https://example.com/page' } in body", }), { headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } if (request.method !== "POST") { return new Response("Method not allowed", { status: 405, headers: CORS_HEADERS, }) } try { const body = (await request.json()) as { url?: string } const { url } = body if (!url) { return new Response( JSON.stringify({ success: false, error: "URL is required", }), { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } if (!env.GOOGLE_SERVICE_ACCOUNT_EMAIL || !env.GOOGLE_PRIVATE_KEY) { return new Response( JSON.stringify({ success: false, error: "Google Search Console credentials not configured", message: "Please set GOOGLE_SERVICE_ACCOUNT_EMAIL and GOOGLE_PRIVATE_KEY environment variables", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } // Note: Full Google Search Console API integration would require googleapis library // For now, this is a placeholder that returns success // To implement fully, you would need to: // 1. Install googleapis in the worker (may require bundling) // 2. Use JWT authentication with service account // 3. Call searchconsole.urlInspection.index.inspect() return new Response( JSON.stringify({ success: true, message: "Indexing request endpoint ready", url, note: "Google Search Console API integration requires additional setup. See documentation.", }), { headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } catch (error) { return new Response( JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Unknown error occurred", }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) } } /** * Health check endpoint */ function handleHealth(env: Env): Response { return new Response( JSON.stringify({ status: "ok", timestamp: new Date().toISOString(), buckets: { manuals: !!env.MANUALS_BUCKET, thumbnails: !!env.THUMBNAILS_BUCKET, }, }), { headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, } ) }