This commit is contained in:
2026-05-17 20:54:53 +05:00
parent 65a0babeab
commit 27eb4fd606
90 changed files with 12343 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
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}