Commit ·
26e335b
1
Parent(s): 28a7b7e
fix: proxy-image handles internal gallery paths; gallery image endpoint needs no auth
Browse files- backend/app/main.py +33 -24
- backend/app/static/assets/index-CSuQU2kS.js +0 -0
- backend/app/static/index.html +1 -1
- frontend/src/Gallery.jsx +4 -12
backend/app/main.py
CHANGED
|
@@ -1087,29 +1087,19 @@ def gallery_list(
|
|
| 1087 |
|
| 1088 |
|
| 1089 |
@app.get("/api/gallery/image/{entry_id}")
|
| 1090 |
-
def gallery_serve_image(
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
|
|
|
| 1096 |
from app.r2 import get_object as r2_get_object
|
| 1097 |
-
from app.
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
elif token:
|
| 1103 |
-
raw_token = token
|
| 1104 |
-
if not raw_token:
|
| 1105 |
-
raise HTTPException(status_code=401, detail="Not authenticated")
|
| 1106 |
-
payload = decode_token(raw_token)
|
| 1107 |
-
if not payload or not payload.get("sub"):
|
| 1108 |
-
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 1109 |
-
username = payload["sub"]
|
| 1110 |
-
if not get_user_by_username(username):
|
| 1111 |
-
raise HTTPException(status_code=401, detail="User not found")
|
| 1112 |
-
entry = gallery_get_entry(username, entry_id)
|
| 1113 |
if not entry:
|
| 1114 |
raise HTTPException(status_code=404, detail="Not found")
|
| 1115 |
r2_key = entry.get("r2_key")
|
|
@@ -1542,16 +1532,35 @@ ALLOWED_IMAGE_PROXY_SUFFIXES = [
|
|
| 1542 |
async def proxy_image(url: str):
|
| 1543 |
"""
|
| 1544 |
Proxy GET request to an R2 presigned URL or Replicate delivery URL.
|
| 1545 |
-
|
| 1546 |
"""
|
| 1547 |
from urllib.parse import urlparse
|
| 1548 |
|
| 1549 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1550 |
parsed = urlparse(url)
|
| 1551 |
host = (parsed.hostname or "").lower()
|
| 1552 |
if not host:
|
| 1553 |
raise HTTPException(status_code=400, detail="Invalid URL: missing hostname")
|
| 1554 |
-
|
| 1555 |
# Check if host ends with any allowed suffix
|
| 1556 |
is_allowed = any(host == s or host.endswith(s) for s in ALLOWED_IMAGE_PROXY_SUFFIXES)
|
| 1557 |
|
|
|
|
| 1087 |
|
| 1088 |
|
| 1089 |
@app.get("/api/gallery/image/{entry_id}")
|
| 1090 |
+
def gallery_serve_image(entry_id: str):
|
| 1091 |
+
"""
|
| 1092 |
+
Serve a gallery image directly from R2. No auth required — the UUID entry_id
|
| 1093 |
+
acts as a capability token (unguessable). This also means old frontend code that
|
| 1094 |
+
routes through /api/proxy-image still works since proxy-image will detect this
|
| 1095 |
+
internal path and serve it directly.
|
| 1096 |
+
"""
|
| 1097 |
from app.r2 import get_object as r2_get_object
|
| 1098 |
+
from app.mongo import get_mongo_db
|
| 1099 |
+
db = get_mongo_db()
|
| 1100 |
+
if db is None:
|
| 1101 |
+
raise HTTPException(status_code=503, detail="Database unavailable")
|
| 1102 |
+
entry = db["gallery_entries"].find_one({"id": entry_id}, {"_id": 0})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
if not entry:
|
| 1104 |
raise HTTPException(status_code=404, detail="Not found")
|
| 1105 |
r2_key = entry.get("r2_key")
|
|
|
|
| 1532 |
async def proxy_image(url: str):
|
| 1533 |
"""
|
| 1534 |
Proxy GET request to an R2 presigned URL or Replicate delivery URL.
|
| 1535 |
+
Also handles internal /api/gallery/image/{id} paths for backwards compat with old frontend.
|
| 1536 |
"""
|
| 1537 |
from urllib.parse import urlparse
|
| 1538 |
|
| 1539 |
try:
|
| 1540 |
+
# Handle internal gallery image paths (old frontend compat)
|
| 1541 |
+
# Old frontend wraps /api/gallery/image/{id} inside proxy-image — serve directly from R2.
|
| 1542 |
+
if url.startswith("/api/gallery/image/"):
|
| 1543 |
+
from app.r2 import get_object as r2_get_object
|
| 1544 |
+
from app.mongo import get_mongo_db
|
| 1545 |
+
entry_id = url.split("/api/gallery/image/")[1].split("?")[0].strip()
|
| 1546 |
+
db = get_mongo_db()
|
| 1547 |
+
entry = db["gallery_entries"].find_one({"id": entry_id}, {"_id": 0}) if db else None
|
| 1548 |
+
if not entry:
|
| 1549 |
+
raise HTTPException(status_code=404, detail="Not found")
|
| 1550 |
+
r2_key = entry.get("r2_key", "")
|
| 1551 |
+
if not r2_key:
|
| 1552 |
+
raise HTTPException(status_code=404, detail="No image")
|
| 1553 |
+
image_bytes = r2_get_object(r2_key)
|
| 1554 |
+
if not image_bytes:
|
| 1555 |
+
raise HTTPException(status_code=404, detail="Image not found in storage")
|
| 1556 |
+
ext = r2_key.lower()
|
| 1557 |
+
ctype = "image/jpeg" if ext.endswith((".jpg", ".jpeg")) else "image/webp" if ext.endswith(".webp") else "image/png"
|
| 1558 |
+
return Response(content=image_bytes, media_type=ctype, headers={"Cache-Control": "private, max-age=3600"})
|
| 1559 |
+
|
| 1560 |
parsed = urlparse(url)
|
| 1561 |
host = (parsed.hostname or "").lower()
|
| 1562 |
if not host:
|
| 1563 |
raise HTTPException(status_code=400, detail="Invalid URL: missing hostname")
|
|
|
|
| 1564 |
# Check if host ends with any allowed suffix
|
| 1565 |
is_allowed = any(host == s or host.endswith(s) for s in ALLOWED_IMAGE_PROXY_SUFFIXES)
|
| 1566 |
|
backend/app/static/assets/index-CSuQU2kS.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/app/static/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 11 |
-
<script type="module" crossorigin src="/assets/index-
|
| 12 |
<link rel="stylesheet" crossorigin href="/assets/index-CUbY3dMk.css">
|
| 13 |
</head>
|
| 14 |
<body>
|
|
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 11 |
+
<script type="module" crossorigin src="/assets/index-CSuQU2kS.js"></script>
|
| 12 |
<link rel="stylesheet" crossorigin href="/assets/index-CUbY3dMk.css">
|
| 13 |
</head>
|
| 14 |
<body>
|
frontend/src/Gallery.jsx
CHANGED
|
@@ -7,20 +7,12 @@ const API_BASE = '/api'
|
|
| 7 |
const AUTH_TOKEN_KEY = 'amalfa_auth_token'
|
| 8 |
const GALLERY_PAGE_SIZE = 48
|
| 9 |
|
| 10 |
-
/**
|
| 11 |
-
* Return the URL to use for displaying an image.
|
| 12 |
-
* Internal gallery image URLs (/api/gallery/image/...) are served directly by
|
| 13 |
-
* our backend with ?token= query param for auth (browser img tags can't send headers).
|
| 14 |
-
* External presigned R2 URLs are still routed through /api/proxy-image for CORS.
|
| 15 |
-
*/
|
| 16 |
function proxyImageUrl(url) {
|
| 17 |
if (!url) return ''
|
| 18 |
-
// Internal backend gallery image
|
| 19 |
-
if (url.startsWith('/api/gallery/image/'))
|
| 20 |
-
|
| 21 |
-
return token ? `${url}?token=${encodeURIComponent(token)}` : url
|
| 22 |
-
}
|
| 23 |
-
// External presigned URL — route through our proxy
|
| 24 |
return `${API_BASE}/proxy-image?url=${encodeURIComponent(url)}`
|
| 25 |
}
|
| 26 |
|
|
|
|
| 7 |
const AUTH_TOKEN_KEY = 'amalfa_auth_token'
|
| 8 |
const GALLERY_PAGE_SIZE = 48
|
| 9 |
|
| 10 |
+
/** Return the display URL for a gallery image or external image. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
function proxyImageUrl(url) {
|
| 12 |
if (!url) return ''
|
| 13 |
+
// Internal backend gallery image — served directly, no auth needed (UUID = capability URL)
|
| 14 |
+
if (url.startsWith('/api/gallery/image/')) return url
|
| 15 |
+
// External presigned URL — route through our proxy to avoid CORS
|
|
|
|
|
|
|
|
|
|
| 16 |
return `${API_BASE}/proxy-image?url=${encodeURIComponent(url)}`
|
| 17 |
}
|
| 18 |
|