from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Any import jwt from fastapi import Depends, HTTPException, Security, status from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer from passlib.context import CryptContext from sqlalchemy import select from sqlalchemy.orm import Session from app.core.config import get_settings from app.core.database import get_db _bearer = HTTPBearer(auto_error=False) _api_key = APIKeyHeader(name="X-API-Token", auto_error=False) _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(password: str) -> str: return _pwd.hash(password) def verify_password(password: str, password_hash: str) -> bool: try: return _pwd.verify(password, password_hash) except Exception: return False def _jwt_secret() -> str: settings = get_settings() return (settings.jwt_secret or settings.api_token or "changeme").strip() def create_access_token(username: str, user_id: int) -> str: settings = get_settings() expire = datetime.now(timezone.utc) + timedelta(hours=settings.jwt_expire_hours) payload = {"sub": username, "uid": user_id, "exp": expire} return jwt.encode(payload, _jwt_secret(), algorithm="HS256") def decode_access_token(token: str) -> dict[str, Any]: try: return jwt.decode(token, _jwt_secret(), algorithms=["HS256"]) except jwt.PyJWTError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", ) from e def require_auth( bearer: HTTPAuthorizationCredentials | None = Security(_bearer), api_key: str | None = Security(_api_key), db: Session = Depends(get_db), ) -> dict[str, Any]: """Accept JWT from login, or legacy shared API_TOKEN.""" from app.models import User settings = get_settings() raw = None if bearer and bearer.credentials: raw = bearer.credentials elif api_key: raw = api_key if not raw: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Требуется вход", ) # Legacy shared token (scripts / migration) if settings.api_token and raw == settings.api_token: return {"username": "api_token", "uid": 0, "legacy": True} payload = decode_access_token(raw) username = str(payload.get("sub") or "") uid = int(payload.get("uid") or 0) user = db.scalar(select(User).where(User.id == uid)) if uid else None if not user or user.username != username: # allow token if user renamed but id matches username in DB user = db.scalar(select(User).where(User.username == username)) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") return {"username": user.username, "uid": user.id, "legacy": False} # Back-compat alias used by older imports require_token = require_auth AuthDep = Depends(require_auth) def ensure_admin_user(db: Session) -> None: from app.models import User settings = get_settings() username = (settings.admin_username or "admin").strip() or "admin" password = (settings.admin_password or settings.api_token or "changeme").strip() or "changeme" user = db.scalar(select(User).where(User.username == username)) if user: return db.add(User(username=username, password_hash=hash_password(password))) db.commit()