Rocky_Mountain_Vending/workers/api-worker/index.ts
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

423 lines
12 KiB
TypeScript

/**
* 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<Response> {
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<Response> {
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<Response> {
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<Response> {
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<Response> {
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' },
}
);
}