sushilideaclan01 commited on
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 CHANGED
@@ -1087,29 +1087,19 @@ def gallery_list(
1087
 
1088
 
1089
  @app.get("/api/gallery/image/{entry_id}")
1090
- def gallery_serve_image(
1091
- entry_id: str,
1092
- token: str | None = None,
1093
- credentials: HTTPAuthorizationCredentials | None = Depends(security),
1094
- ):
1095
- """Serve a gallery image directly from R2. Accepts JWT via Authorization header OR ?token= query param (for img src tags)."""
 
1096
  from app.r2 import get_object as r2_get_object
1097
- from app.auth import decode_token, get_user_by_username
1098
- # Resolve token from header or query param
1099
- raw_token = None
1100
- if credentials and credentials.scheme == "Bearer":
1101
- raw_token = credentials.credentials
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
- Used so the frontend can load images without CORS issues.
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-Dc2J5x3o.js"></script>
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 URL append token for auth
19
- if (url.startsWith('/api/gallery/image/')) {
20
- const token = typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_KEY) : null
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