| import secrets |
| from typing import Annotated |
|
|
| from fastapi import Depends, HTTPException, status |
| from fastapi.security import APIKeyHeader |
|
|
| from app.core.config import settings |
|
|
|
|
| sync_token_header = APIKeyHeader( |
| name="X-Sync-Token", |
| scheme_name="OpportunitySyncToken", |
| description=( |
| "Token interne autorisant la synchronisation " |
| "automatique des opportunités." |
| ), |
| auto_error=False, |
| ) |
|
|
|
|
| async def require_sync_token( |
| provided_token: Annotated[ |
| str | None, |
| Depends(sync_token_header), |
| ], |
| ) -> None: |
| """ |
| Protège les opérations internes de synchronisation. |
| |
| Le token reçu dans l’en-tête X-Sync-Token est comparé |
| au secret SYNC_API_KEY configuré sur le serveur. |
| """ |
|
|
| configured_secret = settings.sync_api_key |
|
|
| if configured_secret is None: |
| raise HTTPException( |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| detail="Opportunity synchronization is not configured.", |
| ) |
|
|
| expected_token = configured_secret.get_secret_value() |
|
|
| if not expected_token: |
| raise HTTPException( |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| detail="Opportunity synchronization is not configured.", |
| ) |
|
|
| if ( |
| provided_token is None |
| or not secrets.compare_digest( |
| provided_token, |
| expected_token, |
| ) |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail="Invalid or missing synchronization token.", |
| headers={ |
| "WWW-Authenticate": "ApiKey", |
| }, |
| ) |
|
|
|
|
| SyncTokenDependency = Annotated[ |
| None, |
| Depends(require_sync_token), |
| ] |