Files
lux_fiscal/backend/app/api/v1/auth.py
T
lauadminandClaude Opus 5.5 b8f2fe6b6e Translate UI and user-facing messages to Ukrainian (#5)
- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format
- Brand "Assistant System" in the top bar and page title
- Backend error details returned to the UI (auth, orders, receipts,
  cash registers, Checkbox/CRM/NP errors) and CLI output
- Tests updated for the new messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:27:39 +03:00

93 lines
3.4 KiB
Python

"""Эндпоинты аутентификации."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request, Response, status
from app.api.deps import CurrentUser, SessionDep
from app.core.security import hash_password, verify_password
from app.db.models.audit import AuditAction
from app.schemas.auth import (
ChangePasswordRequest,
LoginRequest,
LoginResponse,
RefreshRequest,
TokenPair,
UserOut,
)
from app.services import audit, auth
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login", response_model=LoginResponse)
async def login(payload: LoginRequest, request: Request, session: SessionDep) -> LoginResponse:
try:
user = await auth.authenticate(
session, email=payload.email, password=payload.password, request=request
)
except auth.AuthError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
pair = await auth.issue_token_pair(session, user, request=request)
await audit.record(session, action=AuditAction.LOGIN_SUCCESS, user=user, request=request)
await session.commit()
return LoginResponse(**pair.model_dump(), user=UserOut.model_validate(user))
@router.post("/refresh", response_model=TokenPair)
async def refresh(payload: RefreshRequest, request: Request, session: SessionDep) -> TokenPair:
try:
_, pair = await auth.rotate_refresh_token(
session, raw_token=payload.refresh_token, request=request
)
except auth.AuthError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
await session.commit()
return pair
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
payload: RefreshRequest, request: Request, session: SessionDep, user: CurrentUser
) -> Response:
await auth.revoke_refresh_token(session, raw_token=payload.refresh_token)
await audit.record(session, action=AuditAction.LOGOUT, user=user, request=request)
await session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/me", response_model=UserOut)
async def me(user: CurrentUser) -> UserOut:
return UserOut.model_validate(user)
@router.post("/change-password", status_code=status.HTTP_204_NO_CONTENT)
async def change_password(
payload: ChangePasswordRequest, request: Request, session: SessionDep, user: CurrentUser
) -> Response:
if not verify_password(payload.current_password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Поточний пароль вказано неправильно"
)
user.password_hash = hash_password(payload.new_password)
# Смена пароля завершает все остальные сессии: если пароль меняют из-за
# подозрения на компрометацию, чужой refresh-токен обязан перестать работать.
await auth.revoke_all_for_user(session, user.id)
await audit.record(
session,
action=AuditAction.USER_UPDATED,
user=user,
entity_type="user",
entity_id=user.id,
payload={"change": "password"},
request=request,
)
await session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)