mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4368ee0d3c | |||
| 4f9c472a54 | |||
| a6811a3e86 | |||
| 19d322c9d9 |
@@ -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
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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):
|
||||||
"""Модель содержимого токена"""
|
"""Модель содержимого токена"""
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user