from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from jose import JWTError, jwt from datetime import datetime, timedelta from typing import Optional from pydantic import BaseModel import os import bcrypt from database import get_db from models import User from schemas import UserOut router = APIRouter() # JWT настройки SECRET_KEY = os.getenv("JWT_SECRET", "change-me-in-production") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 часа oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): username: Optional[str] = None role: Optional[str] = None def verify_password(plain_password: str, hashed_password: str) -> bool: """Проверка пароля через bcrypt напрямую""" return bcrypt.checkpw( plain_password.encode('utf-8'), hashed_password.encode('utf-8') ) def get_password_hash(password: str) -> str: """Хеширование пароля через bcrypt напрямую""" salt = bcrypt.gensalt() return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): """Создание JWT токена""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def authenticate_user(db: Session, username: str, password: str): """Аутентификация пользователя""" user = db.query(User).filter(User.username == username).first() if not user: return False if not verify_password(password, user.hashed_password): return False return user async def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ) -> User: """Получение текущего пользователя из JWT токена""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username, role=payload.get("role")) except JWTError: raise credentials_exception user = db.query(User).filter(User.username == token_data.username).first() if user is None: raise credentials_exception if not user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return user async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: """Проверка активности пользователя""" if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user def require_role(allowed_roles: list[str]): """Dependency для проверки роли пользователя""" async def role_checker(current_user: User = Depends(get_current_user)): if current_user.role not in allowed_roles: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Access denied. Required roles: {', '.join(allowed_roles)}" ) return current_user return role_checker @router.post("/login", response_model=Token) async def login( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) ): """ Аутентификация и получение JWT токена. Используйте username и password для получения access_token. Токен действителен 24 часа. Тестовые пользователи: - operator / pass123 - field / pass123 - admin / pass123 """ user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) # Обновляем last_login user.last_login = datetime.utcnow() db.commit() access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username, "role": user.role}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} @router.get("/me", response_model=UserOut) async def read_users_me(current_user: User = Depends(get_current_active_user)): """ Получить информацию о текущем пользователе. Требуется валидный JWT токен в заголовке Authorization: Bearer """ return current_user @router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED) async def register_user( username: str, email: str, password: str, full_name: Optional[str] = None, role: str = "operator", current_user: User = Depends(require_role(["admin"])), db: Session = Depends(get_db) ): """ Регистрация нового пользователя (только для admin). Доступные роли: operator, field, admin """ # Проверка существования пользователя if db.query(User).filter(User.username == username).first(): raise HTTPException(status_code=400, detail="Username already registered") if db.query(User).filter(User.email == email).first(): raise HTTPException(status_code=400, detail="Email already registered") # Создание пользователя hashed_password = get_password_hash(password) db_user = User( username=username, email=email, hashed_password=hashed_password, full_name=full_name, role=role ) db.add(db_user) db.commit() db.refresh(db_user) return db_user