Единый тип ответа авторизации, добавление кнопки создания автора на странице авторы

This commit is contained in:
2026-01-31 23:41:56 +03:00
parent dfa4d14afc
commit 19d322c9d9
8 changed files with 50 additions and 43 deletions
+1 -1
View File
@@ -40,5 +40,5 @@ RECOVERY_MIN_REMAINING_WARNING=3
RECOVERY_MAX_AGE_DAYS=365 RECOVERY_MAX_AGE_DAYS=365
# TOTP_2FA # TOTP_2FA
TOTP_ISSUER=LiB TOTP_ISSUER=LiB-local
TOTP_VALID_WINDOW=1 TOTP_VALID_WINDOW=1
+1 -3
View File
@@ -7,7 +7,7 @@ from .role import RoleBase, RoleCreate, RoleList, RoleRead, RoleUpdate
from .user import UserBase, UserCreate, UserList, UserRead, UserUpdate, UserLogin from .user import UserBase, UserCreate, UserList, UserRead, UserUpdate, UserLogin
from .loan import LoanBase, LoanCreate, LoanList, LoanRead, LoanUpdate from .loan import LoanBase, LoanCreate, LoanList, LoanRead, LoanUpdate
from .recovery import RecoveryCodesResponse, RecoveryCodesStatus, RecoveryCodeUse from .recovery import RecoveryCodesResponse, RecoveryCodesStatus, RecoveryCodeUse
from .token import Token, TokenData, PartialToken from .token import TokenData
from .misc import ( from .misc import (
AuthorWithBooks, AuthorWithBooks,
GenreWithBooks, GenreWithBooks,
@@ -62,9 +62,7 @@ __all__ = [
"RoleUpdate", "RoleUpdate",
"RoleRead", "RoleRead",
"RoleList", "RoleList",
"Token",
"TokenData", "TokenData",
"PartialToken",
"TOTPSetupResponse", "TOTPSetupResponse",
"TOTPVerifyRequest", "TOTPVerifyRequest",
"TOTPDisableRequest", "TOTPDisableRequest",
+2 -2
View File
@@ -11,8 +11,8 @@ class AuthorBase(SQLModel):
name: str = Field(description="Псевдоним") name: str = Field(description="Псевдоним")
model_config = ConfigDict( # pyright: ignore model_config = ConfigDict(
json_schema_extra={"example": {"name": "author_name"}} json_schema_extra={"example": {"name": "John Doe"}}
) )
+11
View File
@@ -2,6 +2,7 @@
from typing import List from typing import List
from pydantic import ConfigDict
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
@@ -12,6 +13,16 @@ class RoleBase(SQLModel):
description: str | None = Field(None, description="Описание") description: str | None = Field(None, description="Описание")
payroll: int = Field(0, description="Оплата") payroll: int = Field(0, description="Оплата")
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "admin",
"description": "system administrator",
"payroll": 500,
}
}
)
class RoleCreate(RoleBase): class RoleCreate(RoleBase):
"""Модель роли для создания""" """Модель роли для создания"""
+1 -17
View File
@@ -1,24 +1,8 @@
"""Модуль DTO-моделей токенов""" """Модуль DTO-модели токена"""
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
class Token(SQLModel):
"""Модель токена"""
access_token: str = Field(description="Токен доступа")
token_type: str = Field("bearer", description="Тип токена")
refresh_token: str | None = Field(None, description="Токен обновления")
class PartialToken(SQLModel):
"""Частичный токен — для подтверждения 2FA"""
partial_token: str = Field(description="Частичный токен")
token_type: str = Field("partial", description="Тип токена")
requires_2fa: bool = Field(True, description="Требуется TOTP-код")
class TokenData(SQLModel): class TokenData(SQLModel):
"""Модель содержимого токена""" """Модель содержимого токена"""
+23 -20
View File
@@ -12,15 +12,12 @@ from sqlmodel import Session, select
from library_service.services import require_captcha from library_service.services import require_captcha
from library_service.models.db import Role, User from library_service.models.db import Role, User
from library_service.models.dto import ( from library_service.models.dto import (
Token,
UserCreate, UserCreate,
UserRead, UserRead,
UserUpdate, UserUpdate,
UserList, UserList,
RoleRead, RoleRead,
RoleList, RoleList,
Token,
PartialToken,
LoginResponse, LoginResponse,
RecoveryCodeUse, RecoveryCodeUse,
RegisterResponse, RegisterResponse,
@@ -147,11 +144,14 @@ def login(
) )
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
new_access_token = create_access_token(
data=token_data, expires_delta=access_token_expires
)
new_refresh_token = create_refresh_token(data=token_data)
return LoginResponse( return LoginResponse(
access_token=create_access_token( access_token=new_access_token,
data=token_data, expires_delta=access_token_expires refresh_token=new_refresh_token,
),
refresh_token=create_refresh_token(data=token_data),
token_type="bearer", token_type="bearer",
requires_2fa=False, requires_2fa=False,
) )
@@ -159,7 +159,7 @@ def login(
@router.post( @router.post(
"/refresh", "/refresh",
response_model=Token, response_model=LoginResponse,
summary="Обновление токена", summary="Обновление токена",
description="Получение новой пары токенов, используя действующий Refresh токен", description="Получение новой пары токенов, используя действующий Refresh токен",
) )
@@ -190,19 +190,18 @@ def refresh_token(
detail="User is inactive", detail="User is inactive",
) )
token_data = {"sub": user.username, "user_id": user.id}
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
new_access_token = create_access_token( new_access_token = create_access_token(
data={"sub": user.username, "user_id": user.id}, data=token_data, expires_delta=access_token_expires
expires_delta=access_token_expires,
)
new_refresh_token = create_refresh_token(
data={"sub": user.username, "user_id": user.id}
) )
new_refresh_token = create_refresh_token(data=token_data)
return Token( return LoginResponse(
access_token=new_access_token, access_token=new_access_token,
refresh_token=new_refresh_token, refresh_token=new_refresh_token,
token_type="bearer", token_type="bearer",
requires_2fa=False,
) )
@@ -343,7 +342,7 @@ def disable_2fa(
@router.post( @router.post(
"/2fa/verify", "/2fa/verify",
response_model=Token, response_model=LoginResponse,
summary="Верификация 2FA", summary="Верификация 2FA",
description="Завершает аутентификацию с помощью TOTP кода или резервного кода", description="Завершает аутентификацию с помощью TOTP кода или резервного кода",
) )
@@ -374,12 +373,16 @@ def verify_2fa(
token_data = {"sub": user.username, "user_id": user.id} token_data = {"sub": user.username, "user_id": user.id}
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
new_access_token = create_access_token(
data=token_data, expires_delta=access_token_expires
)
new_refresh_token = create_refresh_token(data=token_data)
return Token( return LoginResponse(
access_token=create_access_token( access_token=new_access_token,
data=token_data, expires_delta=access_token_expires refresh_token=new_refresh_token,
), token_type="bearer",
refresh_token=create_refresh_token(data=token_data), requires_2fa=False,
) )
+5
View File
@@ -6,6 +6,11 @@ $(document).ready(() => {
let currentSort = "name_asc"; let currentSort = "name_asc";
loadAuthors(); loadAuthors();
const USER_CAN_MANAGE =
typeof window.canManage === "function" && window.canManage();
if (USER_CAN_MANAGE) {
$("#add-author-btn").removeClass("hidden");
}
function loadAuthors() { function loadAuthors() {
showLoadingState(); showLoadingState();
+6
View File
@@ -6,6 +6,12 @@
<h2 class="text-2xl font-bold text-gray-800">Авторы</h2> <h2 class="text-2xl font-bold text-gray-800">Авторы</h2>
<div class="flex flex-col sm:flex-row gap-4 w-full md:w-auto"> <div class="flex flex-col sm:flex-row gap-4 w-full md:w-auto">
<a href="/author/create" id="add-author-btn" class="hidden flex justify-center items-center px-4 py-2 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition whitespace-nowrap">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Добавить автора
</a>
<div class="relative"> <div class="relative">
<input <input
type="text" type="text"