fix: sidecar response interceptor + folder proxy
- Added 401-handling response interceptor to sidecar axios instance (matches existing http instance) so expired/invalid tokens redirect to login instead of showing raw 404/401 errors - Added GET /api/sidecar/folders — proxies PhotoPrism's /api/v1/folders/originals with BasePath post-filter - Updated listFolders() frontend to call sidecar proxy - Updated plan with remaining fixes
This commit is contained in:
227
.hermes/plans/2026-06-06_210000-remaining-fixes.md
Normal file
227
.hermes/plans/2026-06-06_210000-remaining-fixes.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Plan: Fix remaining user isolation issues — 404 errors and folder tree
|
||||
|
||||
**Date:** 2026-06-06
|
||||
**Author:** Hermes Agent
|
||||
**Status:** Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Fix the remaining issues after deploying the sidecar scoping proxy:
|
||||
|
||||
1. **404 on photo grid** — "Request failed with status code 404" in private window
|
||||
2. **Folder tree shows other users** — on first load, the library tree lists other users' folders; a refresh fixes it
|
||||
|
||||
## 2. Current Context
|
||||
|
||||
### What's deployed
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Sidecar labels proxy (`/api/sidecar/labels`) | ✅ Working |
|
||||
| Sidecar counts proxy (`/api/sidecar/counts`) | ✅ Working |
|
||||
| Sidecar timeline proxy (`/api/sidecar/timeline`) | ✅ Working through Caddy |
|
||||
| Caddy fallback for `/api/v1/api/sidecar/*` | ✅ Working |
|
||||
| Frontend rebuild with `sidecar` axios instance | ✅ Built and deployed |
|
||||
|
||||
### Verified working via Caddy
|
||||
|
||||
```bash
|
||||
# Through public URL with valid admin token
|
||||
curl https://photos.hubris.network/api/sidecar/timeline?count=1 → HTTP 200
|
||||
curl https://photos.hubris.network/api/v1/photos?count=1 → HTTP 200
|
||||
```
|
||||
|
||||
Both endpoints return 200 when tested directly through Caddy with a valid token.
|
||||
|
||||
### Reported issues
|
||||
|
||||
1. **404 on photo grid** — even in private window (no cache interference)
|
||||
2. **Folder tree shows other users' folders** on first load, fixed by refresh
|
||||
|
||||
## 3. Root Cause Analysis
|
||||
|
||||
### Issue 1: 404 on photo grid
|
||||
|
||||
The `sidecar` axios instance (`baseURL: ''`) is missing the **response interceptor** that:
|
||||
- Handles 401 → clears session → redirects to login
|
||||
- Re-throws with meaningful error message
|
||||
|
||||
The `http` instance (for `/api/v1` endpoints) has this interceptor. Without it on `sidecar`:
|
||||
- If the sidecar returns a non-2xx (401, 502 from upstream PP failure, etc.), axios throws a raw error
|
||||
- The TanStack Query error boundary catches it and shows "Request failed with status code <status>"
|
||||
- Very likely the sidecar is returning 401 on some calls (token expired / session not yet established) and the error message might show 404 because Caddy's catch-all returns 404 when a matcher doesn't find a route
|
||||
|
||||
**Hypothesis:** During OIDC login flow, the frontend may make some sidecar calls BEFORE the session is fully established (token loaded into `session.accessToken`). The `sidecar` interceptor checks `session.accessToken` but it might be null. Then the request to `/api/sidecar/timeline` has no auth header → sidecar returns 401 → no response interceptor → raw error.
|
||||
|
||||
**Fix:** Add the same 401 → login redirect interceptor to the `sidecar` instance.
|
||||
|
||||
### Issue 2: Folder tree shows other users
|
||||
|
||||
`listFolders()` calls `http.get('/folders/originals')` which hits PhotoPrism directly. PhotoPrism returns **all folders across the library** regardless of user. The frontend then filters by `userBasePath()` on the result:
|
||||
|
||||
```typescript
|
||||
const bp = userBasePath();
|
||||
if (bp === '') return folders; // On first load, bp might be empty!
|
||||
return folders.filter((f) => f.Path === bp || f.Path.startsWith(bp + '/'))
|
||||
```
|
||||
|
||||
On first load, `userBasePath()` returns `""` because:
|
||||
1. The session data is loaded asynchronously
|
||||
2. `session.user.BasePath` might not yet be populated when `listFolders` fires
|
||||
3. The TanStack Query cache from a previous session might still have old data
|
||||
|
||||
After a refresh, the session is fully loaded, and `userBasePath()` returns the correct value.
|
||||
|
||||
A secondary issue: the `http` interceptor's 401 handler clears the session on 401. If the session expires during the app's lifetime, all subsequent requests fail with 401.
|
||||
|
||||
## 4. Proposed Approach
|
||||
|
||||
### Phase 1: Fix 404 — add response interceptor to sidecar
|
||||
|
||||
**File:** `web/src/lib/services/photoprism.ts`
|
||||
|
||||
Add the same 401 → login redirect interceptor to `sidecar` as already exists on `http`:
|
||||
|
||||
```typescript
|
||||
sidecar.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err: AxiosError) => {
|
||||
if (err.response?.status === 401 && browser) {
|
||||
clearSession();
|
||||
const url = err.config?.url ?? '';
|
||||
if (!url.endsWith('/session')) {
|
||||
void goto('/login', { replaceState: true });
|
||||
}
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Phase 2: Fix folder tree — sidecar folder proxy
|
||||
|
||||
**File:** `sidecar/handlers_folders.go` (new)
|
||||
|
||||
Add a sidecar endpoint that proxies `/folders/originals` and post-filters by BasePath:
|
||||
|
||||
```
|
||||
GET /api/sidecar/folders → proxies to GET /api/v1/folders/originals
|
||||
→ removes folders not under user's base_path
|
||||
→ returns filtered list
|
||||
```
|
||||
|
||||
This avoids the timing issue entirely by filtering on the server side.
|
||||
|
||||
**Alternative (simpler):** Fix the frontend timing issue by ensuring `listFolders` doesn't fire until the session is ready.
|
||||
|
||||
### Phase 3: Change folder tree in frontend
|
||||
|
||||
**File:** `web/src/lib/services/photoprism.ts`
|
||||
|
||||
Change `listFolders()` to use `sidecar` instance and call `/api/sidecar/folders`:
|
||||
|
||||
```typescript
|
||||
export async function listFolders(): Promise<PpFolder[]> {
|
||||
const { data } = await sidecar.get<{ folders?: PpFolder[] }>(
|
||||
'/api/sidecar/folders',
|
||||
{ params: { recursive: true, uncached: true, files: false } }
|
||||
);
|
||||
const bp = userBasePath();
|
||||
const folders = data.folders ?? [];
|
||||
if (bp === '') return folders;
|
||||
return folders
|
||||
.filter((f) => f.Path === bp || f.Path.startsWith(bp + '/'))
|
||||
.map((f) => ({ ...f, Path: toUserPath(f.Path) }));
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Step-by-step Plan
|
||||
|
||||
### Step 1: Add sidecar response interceptor
|
||||
|
||||
1. Edit `web/src/lib/services/photoprism.ts`
|
||||
2. Add the 401-handling response interceptor to the `sidecar` instance
|
||||
3. The interceptor mirrors the existing `http` response interceptor exactly
|
||||
|
||||
### Step 2: Rebuild frontend
|
||||
|
||||
```bash
|
||||
cd /opt/mule-image/web && npm run build
|
||||
```
|
||||
|
||||
### Step 3: (Optional) Add sidecar folder proxy
|
||||
|
||||
1. New file `sidecar/handlers_folders_proxy.go`
|
||||
2. Handler similar to `handlePhotos` — proxies to `/api/v1/folders/originals`, post-filters by `Path` prefix
|
||||
3. Wire route in `main.go`: `auth.GET("/folders", handleFoldersProxy(pp))`
|
||||
4. Build Docker image, restart sidecar
|
||||
|
||||
### Step 4: Update listFolders to use sidecar
|
||||
|
||||
1. Change `listFolders()` to use `sidecar` instance
|
||||
2. Call `/api/sidecar/folders` instead of `/folders/originals`
|
||||
|
||||
### Step 5: Rebuild + validate
|
||||
|
||||
```bash
|
||||
# Rebuild frontend
|
||||
cd /opt/mule-image/web && npm run build
|
||||
|
||||
# Test through Caddy
|
||||
curl -s "https://photos.hubris.network/api/sidecar/timeline?count=1" \
|
||||
-H "X-Auth-Token: <token>" | head -c 200
|
||||
|
||||
# Verify folders
|
||||
curl -s "https://photos.hubris.network/api/sidecar/folders" \
|
||||
-H "X-Auth-Token: <token>" | python3 -c "import sys,json;d=json.load(sys.stdin);print(json.dumps(d[:3],indent=2))"
|
||||
```
|
||||
|
||||
### Step 6: Commit
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "fix: add sidecar response interceptor + folder proxy" && git push
|
||||
```
|
||||
|
||||
## 6. Files Likely to Change
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `web/src/lib/services/photoprism.ts` | Add response interceptor to sidecar instance; change listFolders URL |
|
||||
| `sidecar/handlers_folders_proxy.go` | **New** — folder proxy handler |
|
||||
| `sidecar/main.go` | Wire folder proxy route |
|
||||
|
||||
## 7. Tests & Validation
|
||||
|
||||
**Manual:**
|
||||
1. Open private window → navigate to photos.hubris.network
|
||||
2. Log in as muli via Authentik OIDC
|
||||
3. Verify photo grid loads without 404
|
||||
4. Verify folder tree shows only muli's folders
|
||||
5. Switch to dtoro account → verify folders/timeline scoped to dtoro
|
||||
|
||||
**API tests:**
|
||||
```bash
|
||||
# Sidecar timeline (no token → 401 redirect)
|
||||
curl -s "https://photos.hubris.network/api/sidecar/timeline?count=1"
|
||||
|
||||
# Sidecar folders
|
||||
curl -s "https://photos.hubris.network/api/sidecar/folders"
|
||||
```
|
||||
|
||||
## 8. Risks & Open Questions
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Sidecar returns 401 during OIDC login flow before session is ready | 404 showing instead of graceful redirect | Add response interceptor in Phase 1 |
|
||||
| Folder proxy adds latency | Slower folder tree loading | Minimal — single proxy call, same as PP direct |
|
||||
| `userBasePath()` timing issue in listFolders persists even with sidecar | Folder tree still shows wrong folders on first load | Sidecar filter is server-side → no timing dependency |
|
||||
|
||||
### Open Questions
|
||||
|
||||
- **Q1**: Are there other API calls that bypass the `sidecar` instance and might also be unscoped? (e.g., `listSubjects`, `listGeo`, etc.)
|
||||
- **Q2**: Does the sidecar need a folder proxy, or is the timing fix sufficient? The timing fix (delaying `listFolders` until session is ready) is simpler but fragile.
|
||||
- **Q3**: Could the 404 be from Caddy's catch-all returning 404 when the sidecar isn't reachable? The Caddy fallback timeout for the sidecar might need tuning.
|
||||
Reference in New Issue
Block a user