fix: scope marks, labels, and subjects to the authenticated user

Marks (ratings/color labels) were stored without a user column — every
user saw every other user's marks. Labels and subjects from PhotoPrism's
global endpoints leaked across users because those endpoints ignore
BasePath ACL.

Sidecar:
- Add UserName as composite primary key on Mark (photo_uid, user_name)
- Replace validateSession with resolveSession that fetches the user
  identity from PhotoPrism's session endpoint
- Filter all mark queries by user_name

Frontend:
- Filter listLabels/listSubjects through a BasePath-aware existence
  check — each label/subject is kept only if the user has at least one
  matching photo (single count=1 probe per item, batched at concurrency 8)
- Skip filtering for admin users with empty BasePath (single-user compat)

Also documents USER_BASEPATHS in .env.example — the env var that drives
per-user library isolation via PhotoPrism's auth_users.base_path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:36:18 +02:00
parent 6c96c22b33
commit 4c08eba27a
6 changed files with 109 additions and 20 deletions

View File

@@ -85,6 +85,37 @@ func (c *ppClient) call(ctx context.Context, method, urlPath, token string, body
}, nil
}
// ppSessionUser is the subset of PhotoPrism's session response we need.
type ppSessionUser struct {
UserName string `json:"UserName"`
UserUID string `json:"UserUID"`
BasePath string `json:"BasePath"`
}
type ppSessionResponse struct {
User ppSessionUser `json:"user"`
}
// resolveSession validates the token AND returns the authenticated user.
// Returns nil when the token is invalid or the response can't be parsed.
func (c *ppClient) resolveSession(ctx context.Context, token string) *ppSessionUser {
if token == "" {
return nil
}
r, err := c.call(ctx, http.MethodGet, "/api/v1/session/"+token, token, nil)
if err != nil || !r.OK {
return nil
}
var resp ppSessionResponse
if err := json.Unmarshal(r.Body, &resp); err != nil {
return nil
}
if resp.User.UserName == "" {
return nil
}
return &resp.User
}
// validateSession is the cheapest probe that the supplied token is live:
// list one photo. 401 → bad/expired token. We never read the payload.
func (c *ppClient) validateSession(ctx context.Context, token string) bool {