Files
devops-portal/backend/auth.py
2026-04-08 00:34:12 +03:00

82 lines
2.4 KiB
Python

from __future__ import annotations
import os
import hashlib
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from backend.db import get_db
from backend.models import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer = HTTPBearer(auto_error=False)
AUTH_VERSION = "prehash-sha256-2026-04-07"
def _env(name: str, default: str) -> str:
value = os.getenv(name)
return value if value else default
JWT_SECRET = _env("JWT_SECRET", "change-me")
JWT_ALG = _env("JWT_ALG", "HS256")
JWT_EXPIRES_MINUTES = int(_env("JWT_EXPIRES_MINUTES", "480")) # 8h
def _bcrypt_safe_password(password: str) -> str:
"""
bcrypt only uses first 72 bytes of the input.
To support long passwords safely, pre-hash using SHA-256 when needed.
"""
raw = password.encode("utf-8")
if len(raw) <= 72:
return password
return hashlib.sha256(raw).hexdigest()
def hash_password(password: str) -> str:
return pwd_context.hash(_bcrypt_safe_password(password))
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(_bcrypt_safe_password(password), password_hash)
def create_access_token(*, user_id: str) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": user_id,
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=JWT_EXPIRES_MINUTES)).timestamp()),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG)
def get_current_user(
creds: HTTPAuthorizationCredentials | None = Depends(bearer),
db: Session = Depends(get_db),
) -> User:
if creds is None or not creds.credentials:
raise HTTPException(status_code=401, detail="Not authenticated")
token = creds.credentials
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG])
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = db.get(User, user_id)
if not user or not user.is_active:
raise HTTPException(status_code=401, detail="User inactive or not found")
return user