Files
mule-image/sidecar/handlers_photos.go
dtoro 14a1b4e54e frontend: route all photo queries through sidecar timeline proxy
- listPhotos → /api/sidecar/timeline (was /photos)
- countPhotos → /api/sidecar/timeline (was /photos)
- hasPhotosMatching → /api/sidecar/timeline (was /photos)
- Sidecar handler forwards X-Count header for countPhotos()
- Sidecar adjusts X-Count to reflect post-filtered count
2026-06-06 20:17:13 +02:00

73 lines
2.0 KiB
Go

package main
import (
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// handlePhotos proxies PhotoPrism's /api/v1/photos and then post-filters
// the response so only photos under the caller's BasePath are returned.
// This fixes the review/archive tab cross-user leak.
//
// Route: GET /api/sidecar/timeline (behind requireSession)
func handlePhotos(pp *ppClient) gin.HandlerFunc {
return func(c *gin.Context) {
token := ctxToken(c)
basePath := ctxBasePath(c)
// Forward the raw query string to PhotoPrism.
query := c.Request.URL.RawQuery
resp, err := pp.call(c.Request.Context(), http.MethodGet, "/api/v1/photos?"+query, token, nil)
if err != nil || !resp.OK {
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream photos request failed"})
return
}
// Decode as a generic array so we can inspect Path without
// committing to a rigid struct (PhotoPrism's photo response
// varies between list/detail/search endpoints).
var photos []map[string]any
if err := json.Unmarshal(resp.Body, &photos); err != nil {
// If it's not an array (e.g. error, single object), pass through.
c.Data(resp.Status, "application/json", resp.Body)
return
}
// If the user has no BasePath (admin/empty), return as-is.
if basePath == "" {
// Forward PhotoPrism's X-Count header for countPhotos().
if count := resp.Header.Get("X-Count"); count != "" {
c.Header("X-Count", count)
}
c.JSON(http.StatusOK, photos)
return
}
prefix := basePath + "/"
// Post-filter by FileName field (originals-relative path).
filtered := make([]map[string]any, 0, len(photos))
for _, ph := range photos {
rawPath, ok := ph["FileName"]
if !ok {
continue
}
pathStr, ok := rawPath.(string)
if !ok {
continue
}
// Match exact basePath or basePath/...
if pathStr == basePath || strings.HasPrefix(pathStr, prefix) {
filtered = append(filtered, ph)
}
}
// Forward X-Count header adjusted to the filtered count.
c.Header("X-Count", itoa(len(filtered)))
c.JSON(http.StatusOK, filtered)
}
}