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>
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
/**
|
|
* Cloudflare Worker for generating signed URLs for R2 objects
|
|
*
|
|
* This worker provides secure access to private R2 buckets by generating
|
|
* time-limited signed URLs.
|
|
*
|
|
* Endpoint: /signed-url?file=<path>&bucket=<bucket>&expires=<seconds>
|
|
*
|
|
* Example:
|
|
* GET /signed-url?file=manuals/Crane/manual.pdf&bucket=vending-vm-manuals&expires=3600
|
|
*
|
|
* Returns: { url: "https://signed-url..." }
|
|
*/
|
|
|
|
export interface Env {
|
|
// R2 bucket bindings (configured in wrangler.toml)
|
|
MANUALS_BUCKET?: R2Bucket;
|
|
THUMBNAILS_BUCKET?: R2Bucket;
|
|
// Default bucket if not specified
|
|
DEFAULT_BUCKET?: R2Bucket;
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
|
|
// Only allow GET requests
|
|
if (request.method !== 'GET') {
|
|
return new Response('Method not allowed', { status: 405 });
|
|
}
|
|
|
|
// Handle signed URL generation
|
|
if (url.pathname === '/signed-url') {
|
|
return handleSignedUrl(request, env, url);
|
|
}
|
|
|
|
// Health check endpoint
|
|
if (url.pathname === '/health') {
|
|
return new Response(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
return new Response('Not found', { status: 404 });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Generate a signed URL for an R2 object
|
|
*/
|
|
async function handleSignedUrl(request: Request, env: Env, url: URL): Promise<Response> {
|
|
const filePath = url.searchParams.get('file');
|
|
const bucketName = url.searchParams.get('bucket') || 'manuals';
|
|
const expiresIn = parseInt(url.searchParams.get('expires') || '3600', 10); // Default 1 hour
|
|
|
|
if (!filePath) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'file parameter is required' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Validate expires parameter
|
|
if (isNaN(expiresIn) || expiresIn < 60 || expiresIn > 604800) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'expires must be between 60 and 604800 seconds (1 minute to 7 days)' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Get the appropriate bucket
|
|
let bucket: R2Bucket | undefined;
|
|
if (bucketName === 'manuals' && env.MANUALS_BUCKET) {
|
|
bucket = env.MANUALS_BUCKET;
|
|
} else if (bucketName === 'thumbnails' && env.THUMBNAILS_BUCKET) {
|
|
bucket = env.THUMBNAILS_BUCKET;
|
|
} else if (env.DEFAULT_BUCKET) {
|
|
bucket = env.DEFAULT_BUCKET;
|
|
} else {
|
|
return new Response(
|
|
JSON.stringify({ error: `Bucket '${bucketName}' not found or not configured` }),
|
|
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
try {
|
|
// Generate signed URL
|
|
// Note: R2 signed URLs are generated using the bucket's presigned URL method
|
|
// This is a simplified example - actual implementation may vary based on R2 API
|
|
const signedUrl = await bucket.createMultipartUpload(filePath, {
|
|
httpMetadata: {
|
|
contentType: getContentType(filePath),
|
|
},
|
|
});
|
|
|
|
// For R2, we need to use the R2 API to generate presigned URLs
|
|
// This is a placeholder - actual R2 signed URL generation may require
|
|
// using the R2 API directly or a different method
|
|
const object = await bucket.get(filePath);
|
|
|
|
if (!object) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'File not found' }),
|
|
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Generate presigned URL (this is a simplified example)
|
|
// In production, you would use R2's actual presigned URL API
|
|
const presignedUrl = await generatePresignedUrl(bucket, filePath, expiresIn);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
url: presignedUrl,
|
|
expiresIn,
|
|
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET',
|
|
},
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error('Error generating signed URL:', error);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Failed to generate signed URL', message: error instanceof Error ? error.message : 'Unknown error' }),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a presigned URL for R2 object
|
|
* Note: This is a placeholder - actual implementation depends on R2 API
|
|
*/
|
|
async function generatePresignedUrl(bucket: R2Bucket, key: string, expiresIn: number): Promise<string> {
|
|
// R2 doesn't have a direct presigned URL API like S3
|
|
// You would need to use R2's public URL with custom domain or
|
|
// implement a custom signing mechanism
|
|
//
|
|
// For now, this returns a placeholder URL
|
|
// In production, you would:
|
|
// 1. Use R2's public bucket URL if bucket is public
|
|
// 2. Or implement custom signing using R2's API
|
|
// 3. Or use a custom domain with R2
|
|
|
|
throw new Error('Presigned URL generation not yet implemented. Use public buckets or implement custom signing.');
|
|
}
|
|
|
|
/**
|
|
* Get content type based on file extension
|
|
*/
|
|
function getContentType(filename: string): string {
|
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
const types: Record<string, string> = {
|
|
'pdf': 'application/pdf',
|
|
'jpg': 'image/jpeg',
|
|
'jpeg': 'image/jpeg',
|
|
'png': 'image/png',
|
|
'webp': 'image/webp',
|
|
'gif': 'image/gif',
|
|
};
|
|
return types[ext || ''] || 'application/octet-stream';
|
|
}
|
|
|
|
|