83b1aeb97c
Adds a shared-token auth dependency for the viewing API (Task 2.9). Trust model: when API_TOKEN is empty (the default) auth is disabled — we trust the network boundary (local/intranet tool). When set, every request must present the token via `Authorization: Bearer <token>` or `?token=<token>`, else 401. `/api/health` is exempt so liveness probes work without the token. - app/config.py: add `api_token: str = ""` to Settings. - app/api/auth.py: `require_auth` dependency (secrets.compare_digest, reads get_settings() per-request so token rotation via cache_clear() works). - app/api/main.py: apply the dependency globally; document the trust model. - .env.example: document API_TOKEN. - tests/test_api_auth.py: 8 tests covering disabled/correct/missing/wrong/ malformed-header/query-param/health-exempt paths. Co-Authored-By: Claude <noreply@anthropic.com>
36 lines
1.2 KiB
Python
36 lines
1.2 KiB
Python
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application settings sourced from environment / .env file."""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
)
|
|
|
|
yunke_base_url: str = "https://crm2.yunkecn.com"
|
|
yunke_username: str = ""
|
|
yunke_password: str = ""
|
|
db_url: str = "mysql+asyncmy://user:pass@localhost:3306/yunke"
|
|
redis_url: str = "redis://localhost:6379/0"
|
|
media_dir: Path = Path("./media")
|
|
# Fernet key for encrypting stored credentials. Required for real runs;
|
|
# empty default lets tests construct Settings without a key.
|
|
encryption_key: str = ""
|
|
# Shared bearer token for the viewing API. Empty (default) -> auth DISABLED
|
|
# (we trust the network boundary; this is a local/intranet tool). When set,
|
|
# every API request must present this value via ``Authorization: Bearer
|
|
# <token>`` or ``?token=<token>``. See app/api/auth.py.
|
|
api_token: str = ""
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
"""Return the process-wide Settings singleton."""
|
|
return Settings()
|