Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

View File

@@ -0,0 +1,379 @@
# Media Authentication Fix
**Date:** 2025-10-31
**Issue:** Media thumbnails and images broken after adding authentication
**Status:** ✅ FIXED
---
## Problem
After implementing authentication on all API endpoints, media thumbnails and images stopped loading in the frontend. The issue was that `<img>` and `<video>` HTML tags cannot send Authorization headers, which are required for Bearer token authentication.
### Error Symptoms
- All thumbnails showing as broken images
- Preview images not loading in lightbox
- Video previews failing to load
- Browser console: HTTP 401 Unauthorized errors
### Root Cause
```typescript
// Frontend code using img tags
<img src={api.getMediaThumbnailUrl(filePath, mediaType)} />
// The API returns just a URL string
getMediaThumbnailUrl(filePath: string, mediaType: string) {
return `/api/media/thumbnail?file_path=${filePath}&media_type=${mediaType}`
}
```
The browser makes a direct GET request for the image without any auth headers:
```
GET /api/media/thumbnail?file_path=...
(No Authorization header)
→ HTTP 401 Unauthorized
```
---
## Solution
### 1. Backend: Query Parameter Token Support
Created a new authentication dependency that accepts tokens via query parameters in addition to Authorization headers:
```python
async def get_current_user_media(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
token: Optional[str] = None
) -> Dict:
"""
Authentication for media endpoints that supports both header and query parameter tokens.
This allows <img> and <video> tags to work by including token in URL.
"""
auth_token = None
# Try to get token from Authorization header first
if credentials:
auth_token = credentials.credentials
# Fall back to query parameter
elif token:
auth_token = token
if not auth_token:
raise HTTPException(status_code=401, detail="Not authenticated")
payload = app_state.auth.verify_session(auth_token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload
```
**Applied to endpoints:**
- `/api/media/thumbnail` - Get or generate thumbnails
- `/api/media/preview` - Serve full media files
**Updated signatures:**
```python
# Before
async def get_media_thumbnail(
request: Request,
current_user: Dict = Depends(get_current_user),
file_path: str = None,
media_type: str = None
):
# After
async def get_media_thumbnail(
request: Request,
file_path: str = None,
media_type: str = None,
token: str = None, # NEW: query parameter
current_user: Dict = Depends(get_current_user_media) # NEW: supports query param
):
```
### 2. Frontend: Append Tokens to URLs
Updated API utility functions to append authentication tokens to media URLs:
```typescript
// Before
getMediaPreviewUrl(filePath: string) {
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}`
}
// After
getMediaPreviewUrl(filePath: string) {
const token = localStorage.getItem('auth_token')
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : ''
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}${tokenParam}`
}
```
Now when the browser loads an image:
```html
<img src="/api/media/thumbnail?file_path=...&media_type=image&token=eyJhbGci..." />
```
The token is included in the URL, and the backend can authenticate the request.
---
## Security Considerations
### Token in URL Query Parameters
**Concerns:**
- Tokens visible in browser history
- Tokens may appear in server logs
- Tokens could leak via Referer header
**Mitigations:**
1. **Rate limiting** - Media endpoints limited to 100 requests/minute
2. **Token expiration** - JWT tokens expire after 24 hours
3. **Session tracking** - Sessions stored in database, can be revoked
4. **HTTPS** - Already handled by nginx proxy, encrypts URLs in transit
5. **Limited scope** - Only applies to media endpoints, not sensitive operations
**Alternatives considered:**
1.**Make media public** - Defeats authentication purpose
2.**Cookie-based auth** - Requires CSRF protection, more complex
3.**Token in query param** - Simple, works with img/video tags, acceptable risk
### Best Practices Applied
✅ Header authentication preferred (checked first)
✅ Query param fallback only for media
✅ Token validation same as header auth
✅ Session tracking maintained
✅ Rate limiting enforced
✅ HTTPS encryption in place
---
## Testing Results
### Thumbnail Endpoint
```bash
# With token
curl "http://localhost:8000/api/media/thumbnail?file_path=/path/to/image.jpg&media_type=image&token=JWT_TOKEN"
→ HTTP 200 (returns JPEG thumbnail)
# Without token
curl "http://localhost:8000/api/media/thumbnail?file_path=/path/to/image.jpg&media_type=image"
→ HTTP 401 {"detail":"Not authenticated"}
```
### Preview Endpoint
```bash
# With token
curl "http://localhost:8000/api/media/preview?file_path=/path/to/video.mp4&token=JWT_TOKEN"
→ HTTP 200 (returns video file)
# Without token
curl "http://localhost:8000/api/media/preview?file_path=/path/to/video.mp4"
→ HTTP 401 {"detail":"Not authenticated"}
```
### Frontend
✅ Thumbnails loading in Downloads page
✅ Thumbnails loading in Media Gallery
✅ Lightbox preview working for images
✅ Video playback working
✅ Token automatically appended to URLs
✅ No console errors
---
## Files Modified
### Backend
**File:** `/opt/media-downloader/web/backend/api.py`
1. **Added new auth dependency** (line ~131):
```python
async def get_current_user_media(...)
```
2. **Updated `/api/media/thumbnail` endpoint** (line ~1921):
- Added `token: str = None` parameter
- Changed auth from `get_current_user` to `get_current_user_media`
3. **Updated `/api/media/preview` endpoint** (line ~1957):
- Added `token: str = None` parameter
- Changed auth from `get_current_user` to `get_current_user_media`
### Frontend
**File:** `/opt/media-downloader/web/frontend/src/lib/api.ts`
1. **Updated `getMediaPreviewUrl()`** (line ~435):
- Reads token from localStorage
- Appends `&token=...` to URL if token exists
2. **Updated `getMediaThumbnailUrl()`** (line ~441):
- Reads token from localStorage
- Appends `&token=...` to URL if token exists
---
## Alternative Approaches
### Option 1: Blob URLs with Fetch (Most Secure)
```typescript
async function getMediaThumbnailUrl(filePath: string, mediaType: string) {
const response = await fetch(`/api/media/thumbnail?file_path=${filePath}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
const blob = await response.blob()
return URL.createObjectURL(blob)
}
```
**Pros:**
- Token never in URL
- Most secure approach
- Standard authentication
**Cons:**
- More complex implementation
- Requires updating all components
- Memory management for blob URLs
- Extra network requests
**Future consideration:** If security requirements increase, this approach should be implemented.
### Option 2: Cookie-Based Authentication
Set JWT as HttpOnly cookie instead of localStorage.
**Pros:**
- Automatic inclusion in requests
- Works with img/video tags
- HttpOnly protects from XSS
**Cons:**
- Requires CSRF protection
- More complex cookie handling
- Domain/path considerations
- Mobile app compatibility issues
---
## Monitoring
### Check for Token Leakage
**Server logs:**
```bash
# Check if tokens appearing in access logs
sudo grep "token=" /var/log/nginx/access.log | head -5
```
If tokens are being logged, update nginx config to filter query parameters from logs.
**Rate limit monitoring:**
```bash
# Check for suspicious media access patterns
sudo journalctl -u media-downloader-api | grep "media/thumbnail"
```
### Security Audit
Run periodic checks:
```bash
# Test unauthenticated access blocked
curl -s "http://localhost:8000/api/media/thumbnail?file_path=/test.jpg&media_type=image"
# Should return: {"detail":"Not authenticated"}
# Test rate limiting
for i in {1..110}; do
curl -s "http://localhost:8000/api/media/thumbnail?..."
done
# Should hit rate limit after 100 requests
```
---
## Deployment Notes
### Service Restart
```bash
# API backend
sudo systemctl restart media-downloader-api
# Frontend (if using systemd service)
sudo systemctl restart media-downloader-frontend
# Or if using vite dev server, it auto-reloads
```
### Verification
1. **Login to application**
2. **Navigate to Downloads or Media page**
3. **Verify thumbnails loading**
4. **Click thumbnail to open lightbox**
5. **Verify full image/video loads**
6. **Check browser console for no errors**
---
## Future Improvements
1. **Blob URL Implementation**
- More secure, tokens not in URL
- Requires frontend refactoring
2. **Token Rotation**
- Short-lived tokens for media access
- Separate media access tokens
3. **Watermarking**
- Add user watermark to previews
- Deter unauthorized sharing
4. **Access Logging**
- Log who accessed what media
- Analytics dashboard
5. **Progressive Loading**
- Blur placeholder while loading
- Better UX during auth check
---
## Rollback Procedure
If issues occur, revert changes:
```bash
# Backend
cd /opt/media-downloader
git checkout HEAD~1 web/backend/api.py
# Frontend
git checkout HEAD~1 web/frontend/src/lib/api.ts
# Restart services
sudo systemctl restart media-downloader-api
```
**Note:** This will make media endpoints unauthenticated again. Only use in emergency.
---
## Summary
**Issue:** Media broken due to authentication on img/video tag endpoints
**Solution:** Support token in query parameter for media endpoints
**Testing:** Both thumbnail and preview endpoints work with token parameter
**Security:** Acceptable risk given rate limiting, HTTPS, and token expiration
**Status:** Fully operational
**Impact:** Media gallery and thumbnails now working with authentication maintained.