Compare commits

...

3 Commits

Author SHA1 Message Date
f05958ed36 J'ai :
All checks were successful
Build and Push Docker Images / Build Backend Image (push) Successful in 9m36s
Build and Push Docker Images / Build Frontend Image (push) Successful in 7m23s
Nettoyé les logs de debug dans backend/src/core/auth.py - supprimé tous les logger.info/warning de la fonction authenticate_user
Ajouté les tokens JWT à toutes les requêtes du player :
frontend/components/AudioPlayer.tsx : Ajouté Authorization header à loadWaveform()
frontend/components/AudioPlayer.tsx : Créé getAuthenticatedStreamUrl() qui ajoute le token en query param pour les <audio> et <a> tags
backend/src/api/routes/audio.py : Ajouté support du token en query param pour /stream et /download (compatibilité avec les tags HTML qui ne supportent pas les headers)
Le player devrait maintenant fonctionner entièrement avec l'authentification.
2025-12-26 17:46:39 +01:00
aa252487b8 Fix: Ajouter Authorization header aux requêtes fetch du scan
Problème: Les requêtes fetch() vers /api/library/scan utilisaient
pas l'interceptor axios, donc le token JWT n'était pas envoyé.
Résultat: 403 Forbidden

Solution: Ajouter manuellement le header Authorization avec le token
depuis localStorage pour les requêtes fetch du rescan.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-26 17:42:54 +01:00
ed7034f55b Fix: Passer les variables d'auth au container backend
Problème: Les variables ADMIN_EMAIL, ADMIN_PASSWORD, JWT_SECRET_KEY
étaient dans le .env mais n'étaient PAS passées au container Docker.
Le backend utilisait donc les valeurs par défaut.

Solution: Ajouter les 4 variables d'auth dans docker-compose.yml
- ADMIN_EMAIL
- ADMIN_PASSWORD
- JWT_SECRET_KEY
- JWT_EXPIRATION_HOURS

Maintenant le container charge les variables depuis le .env du serveur.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-26 17:39:44 +01:00
5 changed files with 49 additions and 14 deletions

View File

@@ -1,13 +1,15 @@
"""Audio streaming and download endpoints."""
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from uuid import UUID
from pathlib import Path
from typing import Optional
from ...models.database import get_db
from ...models import crud
from ...core.waveform_generator import get_waveform_data
from ...core.auth import verify_token
from ...utils.logging import get_logger
router = APIRouter()
@@ -18,6 +20,7 @@ logger = get_logger(__name__)
async def stream_audio(
track_id: UUID,
request: Request,
token: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""Stream audio file with range request support.
@@ -28,6 +31,7 @@ async def stream_audio(
Args:
track_id: Track UUID
request: HTTP request
token: Optional JWT token for authentication (for <audio> tag compatibility)
db: Database session
Returns:
@@ -36,6 +40,9 @@ async def stream_audio(
Raises:
HTTPException: 404 if track not found or file doesn't exist
"""
# Verify authentication via query parameter for <audio> tag
if token:
verify_token(token)
track = crud.get_track_by_id(db, track_id)
if not track:
@@ -79,12 +86,14 @@ async def stream_audio(
@router.get("/download/{track_id}")
async def download_audio(
track_id: UUID,
token: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""Download audio file.
Args:
track_id: Track UUID
token: Optional JWT token for authentication (for <a> tag compatibility)
db: Database session
Returns:
@@ -93,6 +102,9 @@ async def download_audio(
Raises:
HTTPException: 404 if track not found or file doesn't exist
"""
# Verify authentication via query parameter for <a> tag
if token:
verify_token(token)
track = crud.get_track_by_id(db, track_id)
if not track:

View File

@@ -100,22 +100,13 @@ def authenticate_user(email: str, password: str) -> Optional[dict]:
Returns:
User data if authenticated, None otherwise
"""
# Debug logging (remove in production)
logger.info(f"Auth attempt - Email provided: '{email}'")
logger.info(f"Auth attempt - Expected email: '{settings.ADMIN_EMAIL}'")
logger.info(f"Auth attempt - Email match: {email == settings.ADMIN_EMAIL}")
logger.info(f"Auth attempt - Password length: {len(password)}")
logger.info(f"Auth attempt - Expected password length: {len(settings.ADMIN_PASSWORD)}")
# Check against admin credentials from environment
if email == settings.ADMIN_EMAIL and password == settings.ADMIN_PASSWORD:
logger.info(f"✅ Authentication successful for {email}")
return {
"email": email,
"role": "admin"
}
logger.warning(f"❌ Authentication failed for {email}")
return None

View File

@@ -30,6 +30,11 @@ services:
ANALYSIS_USE_CLAP: ${ANALYSIS_USE_CLAP:-false}
ANALYSIS_NUM_WORKERS: ${ANALYSIS_NUM_WORKERS:-4}
ESSENTIA_MODELS_PATH: /app/models
# Authentication
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@example.com}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-changeme}
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-secret-key-change-in-production}
JWT_EXPIRATION_HOURS: ${JWT_EXPIRATION_HOURS:-24}
ports:
- "8001:8000"
volumes:

View File

@@ -91,8 +91,17 @@ export default function Home() {
setIsScanning(true)
setScanStatus("Démarrage du scan...")
const token = localStorage.getItem('access_token')
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const response = await fetch(`${getApiUrl()}/api/library/scan`, {
method: 'POST',
headers,
})
if (!response.ok) {
@@ -104,7 +113,9 @@ export default function Home() {
// Poll scan status
const pollInterval = setInterval(async () => {
try {
const statusResponse = await fetch(`${getApiUrl()}/api/library/scan/status`)
const statusResponse = await fetch(`${getApiUrl()}/api/library/scan/status`, {
headers,
})
const status = await statusResponse.json()
if (!status.is_scanning) {

View File

@@ -79,8 +79,15 @@ export default function AudioPlayer({ track, isPlaying, onPlayingChange }: Audio
const loadWaveform = async (trackId: string) => {
setIsLoadingWaveform(true)
try {
const token = localStorage.getItem('access_token')
const headers: HeadersInit = {}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const response = await fetch(
`${getApiUrl()}/api/audio/waveform/${trackId}`
`${getApiUrl()}/api/audio/waveform/${trackId}`,
{ headers }
)
if (response.ok) {
const data = await response.json()
@@ -159,10 +166,19 @@ export default function AudioPlayer({ track, isPlaying, onPlayingChange }: Audio
const progress = duration > 0 ? (currentTime / duration) * 100 : 0
const getAuthenticatedStreamUrl = (trackId: string) => {
const token = localStorage.getItem('access_token')
const baseUrl = `${getApiUrl()}/api/audio/stream/${trackId}`
if (token) {
return `${baseUrl}?token=${encodeURIComponent(token)}`
}
return baseUrl
}
return (
<div className="bg-gray-50 border-t border-gray-300 shadow-lg" style={{ height: '80px' }}>
{/* Hidden audio element */}
{track && <audio ref={audioRef} src={`${getApiUrl()}/api/audio/stream/${track.id}`} />}
{track && <audio ref={audioRef} src={getAuthenticatedStreamUrl(track.id)} />}
<div className="h-full flex items-center gap-3 px-4">
{/* Play/Pause button */}
@@ -301,7 +317,7 @@ export default function AudioPlayer({ track, isPlaying, onPlayingChange }: Audio
{/* Download button */}
{track && (
<a
href={`${getApiUrl()}/api/audio/download/${track.id}`}
href={getAuthenticatedStreamUrl(track.id).replace('/stream/', '/download/')}
download
className="w-8 h-8 flex items-center justify-center text-gray-600 hover:text-gray-900 transition-colors rounded hover:bg-gray-200 flex-shrink-0"
aria-label="Download"