Files
vector/backend/api/v1/auth.py
T
2026-06-06 18:31:55 +00:00

197 lines
6.4 KiB
Python

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 <token>
"""
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