diff --git a/.env.example b/.env.example index cbbe7e0..c12db60 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= S3_PUBLIC_BASE_URL= S3_KEY_PREFIX=bird-photos +PHOTO_DELIVERY_MODE=proxy FRONTEND_URL=http://localhost:3000 BACKEND_URL=http://localhost:5000 VITE_API_BASE_URL=http://localhost:5000/api diff --git a/README.md b/README.md index 5be53ff..9cb26ff 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,9 @@ Set these when Wasabi image storage is ready: - `S3_SECRET_ACCESS_KEY=` - `S3_PUBLIC_BASE_URL=` - `S3_KEY_PREFIX=bird-photos` +- `PHOTO_DELIVERY_MODE=proxy` -Use a dedicated private bucket and access key for FlockPal images. Grant only the S3 permissions the app needs for that bucket. When `S3_PUBLIC_BASE_URL` is blank, FlockPal stores private object keys and returns short-lived signed URLs for bird photos. +Use a dedicated private bucket and access key for FlockPal images. Grant only the S3 permissions the app needs for that bucket. When `S3_PUBLIC_BASE_URL` is blank, FlockPal stores private object keys. `PHOTO_DELIVERY_MODE=proxy` streams images through the backend after validating the app photo token; `PHOTO_DELIVERY_MODE=redirect` validates the app token and redirects to a short-lived Wasabi signed URL. Migrate existing Postgres-stored bird photos after deploying S3 image storage: diff --git a/backend/src/app.ts b/backend/src/app.ts index 7928140..e6d988d 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -134,6 +134,7 @@ const trustProxy = process.env.TRUST_PROXY?.trim() ?? ''; const milestoneRemindersEnabled = (process.env.MILESTONE_REMINDERS_ENABLED ?? 'true').toLowerCase() !== 'false'; const milestoneReminderTimeZone = process.env.MILESTONE_REMINDER_TIME_ZONE?.trim() || 'America/New_York'; const milestoneReminderCheckIntervalMs = 60 * 60 * 1000; +const photoDeliveryMode = process.env.PHOTO_DELIVERY_MODE === 'redirect' ? 'redirect' : 'proxy'; if (trustProxy) { app.set('trust proxy', trustProxy === 'true' ? true : Number(trustProxy) || trustProxy); @@ -2766,8 +2767,31 @@ app.get('/api/birds/:birdId/photo', async (req: Request, res: Response, next: Ne expiresInSeconds: 5 * 60, }); - res.setHeader('Cache-Control', 'private, max-age=300'); - res.redirect(302, signedUrl); + res.setHeader('Cache-Control', 'private, max-age=900'); + + if (photoDeliveryMode === 'redirect') { + res.redirect(302, signedUrl); + return; + } + + const imageResponse = await fetch(signedUrl); + + if (!imageResponse.ok) { + res.status(imageResponse.status).json({ error: 'Unable to load bird photo.' }); + return; + } + + const contentType = imageResponse.headers.get('content-type') || bird.photo_content_type || 'application/octet-stream'; + const contentLength = imageResponse.headers.get('content-length'); + const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); + + res.setHeader('Content-Type', contentType); + + if (contentLength) { + res.setHeader('Content-Length', contentLength); + } + + res.send(imageBuffer); } catch (error) { next(error); } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 63ae0cc..167a94b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -51,6 +51,7 @@ services: S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_PUBLIC_BASE_URL: ${S3_PUBLIC_BASE_URL:-} S3_KEY_PREFIX: ${S3_KEY_PREFIX:-bird-photos} + PHOTO_DELIVERY_MODE: ${PHOTO_DELIVERY_MODE:-proxy} FRONTEND_URL: ${FRONTEND_URL:?set FRONTEND_URL for production} BACKEND_URL: ${BACKEND_URL:?set BACKEND_URL for production} ADMIN_EMAILS: ${ADMIN_EMAILS:-} @@ -127,6 +128,7 @@ services: S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_PUBLIC_BASE_URL: ${S3_PUBLIC_BASE_URL:-} S3_KEY_PREFIX: ${S3_KEY_PREFIX:-bird-photos} + PHOTO_DELIVERY_MODE: ${PHOTO_DELIVERY_MODE:-proxy} FRONTEND_URL: ${FRONTEND_URL:?set FRONTEND_URL for production} BACKEND_URL: ${BACKEND_URL:?set BACKEND_URL for production} ADMIN_EMAILS: ${ADMIN_EMAILS:-} diff --git a/docker-compose.yml b/docker-compose.yml index 1e8ce9c..b7cd0cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ services: S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_PUBLIC_BASE_URL: ${S3_PUBLIC_BASE_URL:-} S3_KEY_PREFIX: ${S3_KEY_PREFIX:-bird-photos} + PHOTO_DELIVERY_MODE: ${PHOTO_DELIVERY_MODE:-proxy} FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000} BACKEND_URL: ${BACKEND_URL:-http://localhost:5000} ADMIN_EMAILS: ${ADMIN_EMAILS:-} @@ -120,6 +121,7 @@ services: S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_PUBLIC_BASE_URL: ${S3_PUBLIC_BASE_URL:-} S3_KEY_PREFIX: ${S3_KEY_PREFIX:-bird-photos} + PHOTO_DELIVERY_MODE: ${PHOTO_DELIVERY_MODE:-proxy} FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000} BACKEND_URL: ${BACKEND_URL:-http://localhost:5000} ADMIN_EMAILS: ${ADMIN_EMAILS:-}