sidecar: scoped photos/timeline proxy (fixes review + archive leak)
- New GET /api/sidecar/timeline — proxies PP's /api/v1/photos and post-filters by FileName prefix matching the user's BasePath - Also works for review/archive views (q=review:true, q=archived:true) - Frontend route uses /timeline to avoid Gin route conflict with existing /photos/:uid/marks pattern
This commit is contained in:
67
sidecar/handlers_photos.go
Normal file
67
sidecar/handlers_photos.go
Normal file
@@ -0,0 +1,67 @@
|
||||
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 == "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, filtered)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user