Files
GostGenerator/web/backend/app/core/auth.py
T
2026-09-08 10:10:30 +03:00

107 lines
3.4 KiB
Python

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()