Encrypt file paths in API URLs using Fernet tokens

Raw filesystem paths were exposed in browser URLs, dev tools, and proxy logs.
Now all file-serving endpoints accept an opaque encrypted token (t= param)
derived from the session secret via HKDF, with a 4-hour TTL.

Backend:
- Add core/path_tokens.py with Fernet encrypt/decrypt (HKDF from .session_secret)
- Add file_token to all list/gallery/feed/search responses across 7 routers
- Accept optional t= param on all file-serving endpoints (backward compatible)

Frontend:
- Update 4 URL helpers in api.ts to prefer token when available
- Add 4 new helpers for paid-content/embedded-metadata URLs
- Update all 14 page/component files to pass file_token to URL builders
- Add file_token to all relevant TypeScript interfaces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-30 08:25:22 -04:00
parent 523f91788e
commit 49e72207bf
24 changed files with 295 additions and 65 deletions

View File

@@ -10,6 +10,7 @@ from slowapi import Limiter
from slowapi.util import get_remote_address
from ..core.dependencies import get_current_user, get_app_state
from ..core.exceptions import handle_exceptions
from ..core.path_tokens import encode_path
from modules.universal_logger import get_logger
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
@@ -76,9 +77,11 @@ async def get_recent_items(
media_items = []
for row in cursor.fetchall():
fp = row[1]
media_items.append({
'id': row[0],
'file_path': row[1],
'file_path': fp,
'file_token': encode_path(fp) if fp else None,
'filename': row[2],
'source': row[3],
'platform': row[4],
@@ -152,9 +155,11 @@ async def get_recent_items(
'matched_person': row[13]
}
fp = row[1]
review_items.append({
'id': row[0],
'file_path': row[1],
'file_path': fp,
'file_token': encode_path(fp) if fp else None,
'filename': row[2],
'source': row[3],
'platform': row[4],
@@ -300,5 +305,6 @@ async def set_dismissed_cards(
preference_value = excluded.preference_value,
updated_at = CURRENT_TIMESTAMP
""", (user_id, json.dumps(data)))
conn.commit()
return {'status': 'ok'}