Compare commits

..

4 Commits

9 changed files with 50 additions and 87 deletions
-44
View File
@@ -1,44 +0,0 @@
# Postgres
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=lib
# Ollama
OLLAMA_URL="http://localhost:11434"
OLLAMA_MAX_LOADED_MODELS=1
OLLAMA_NUM_THREADS=4
OLLAMA_KEEP_ALIVE=5m
# Default admin account
# DEFAULT_ADMIN_USERNAME="admin"
# DEFAULT_ADMIN_EMAIL="admin@example.com"
# DEFAULT_ADMIN_PASSWORD="password-is-generated-randomly-on-first-launch"
SECRET_KEY="your-secret-key-change-in-production"
DOMAIN=localhost
# JWT
ALGORITHM=HS256
REFRESH_TOKEN_EXPIRE_DAYS=7
ACCESS_TOKEN_EXPIRE_MINUTES=15
PARTIAL_TOKEN_EXPIRE_MINUTES=5
# Hash
ARGON2_TYPE=id
ARGON2_TIME_COST=3
ARGON2_MEMORY_COST=65536
ARGON2_PARALLELISM=4
ARGON2_SALT_LENGTH=16
ARGON2_HASH_LENGTH=48
# Recovery codes
RECOVERY_CODES_COUNT=10
RECOVERY_CODE_SEGMENTS=4
RECOVERY_CODE_SEGMENT_BYTES=2
RECOVERY_MIN_REMAINING_WARNING=3
RECOVERY_MAX_AGE_DAYS=365
# TOTP_2FA
TOTP_ISSUER=LiB
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"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "LiB" name = "LiB"
version = "0.8.0" version = "0.8.1"
description = "Это простое API для управления авторами, книгами и их жанрами." description = "Это простое API для управления авторами, книгами и их жанрами."
authors = [{ name = "wowlikon" }] authors = [{ name = "wowlikon" }]
readme = "README.md" readme = "README.md"