Rocky_Mountain_Vending/workers/r2-signed-urls/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

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';
}