106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ...core.db import get_db
|
|
from pydantic import BaseModel
|
|
|
|
from ...core.security import (
|
|
create_access_token,
|
|
create_refresh_token,
|
|
decode_token,
|
|
hash_password,
|
|
verify_password,
|
|
)
|
|
from ...models.user import User
|
|
from ...schemas.auth import LoginIn, RefreshIn, TokenPair, UserOut
|
|
from ...services.events import add_audit
|
|
from ..deps import get_current_user
|
|
|
|
|
|
class ChangePasswordIn(BaseModel):
|
|
current: str
|
|
new: str
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _issue(user: User) -> TokenPair:
|
|
return TokenPair(
|
|
access_token=create_access_token(user.id, extra={"role": user.role}),
|
|
refresh_token=create_refresh_token(user.id),
|
|
)
|
|
|
|
|
|
def _client_ip(req: Request) -> str | None:
|
|
fwd = req.headers.get("x-forwarded-for")
|
|
if fwd:
|
|
return fwd.split(",")[0].strip()
|
|
return req.client.host if req.client else None
|
|
|
|
|
|
@router.post("/login", response_model=TokenPair)
|
|
def login_json(payload: LoginIn, request: Request, db: Session = Depends(get_db)) -> TokenPair:
|
|
user = db.query(User).filter(User.email == payload.email).first()
|
|
ip = _client_ip(request)
|
|
if not user or not verify_password(payload.password, user.hashed_password):
|
|
add_audit(db, actor=payload.email, action="login.fail", ip=ip, detail="invalid credentials")
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid credentials")
|
|
if not user.is_active:
|
|
add_audit(db, actor=payload.email, action="login.fail", ip=ip, detail="user disabled")
|
|
raise HTTPException(status.HTTP_403_FORBIDDEN, "user disabled")
|
|
add_audit(db, actor=user.email, action="login.success", ip=ip)
|
|
return _issue(user)
|
|
|
|
|
|
@router.post("/login/form", response_model=TokenPair, include_in_schema=False)
|
|
def login_form(
|
|
request: Request,
|
|
form: OAuth2PasswordRequestForm = Depends(),
|
|
db: Session = Depends(get_db),
|
|
) -> TokenPair:
|
|
"""Совместимость со Swagger «Authorize»."""
|
|
user = db.query(User).filter(User.email == form.username).first()
|
|
ip = _client_ip(request)
|
|
if not user or not verify_password(form.password, user.hashed_password):
|
|
add_audit(db, actor=form.username, action="login.fail", ip=ip, detail="invalid credentials")
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid credentials")
|
|
add_audit(db, actor=user.email, action="login.success", ip=ip)
|
|
return _issue(user)
|
|
|
|
|
|
@router.post("/refresh", response_model=TokenPair)
|
|
def refresh(payload: RefreshIn, db: Session = Depends(get_db)) -> TokenPair:
|
|
try:
|
|
data = decode_token(payload.refresh_token)
|
|
except ValueError as exc:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc
|
|
if data.get("type") != "refresh":
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "wrong token type")
|
|
user = db.query(User).filter(User.id == int(data["sub"])).first()
|
|
if not user or not user.is_active:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "user not found")
|
|
return _issue(user)
|
|
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
def me(user: User = Depends(get_current_user)) -> User:
|
|
return user
|
|
|
|
|
|
@router.post("/change-password")
|
|
def change_password(
|
|
payload: ChangePasswordIn,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
) -> dict[str, bool]:
|
|
if not verify_password(payload.current, user.hashed_password):
|
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Текущий пароль неверный")
|
|
if len(payload.new) < 4:
|
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Новый пароль слишком короткий")
|
|
user.hashed_password = hash_password(payload.new)
|
|
db.commit()
|
|
return {"ok": True}
|