Compare commits
8 Commits
807dbee647
...
main
Author | SHA1 | Date | |
---|---|---|---|
5a299cf47d | |||
170492a5c2 | |||
ae14c51b0e | |||
78cc7e3f54 | |||
40d45e8379 | |||
28acd7d04d | |||
e2d0669064 | |||
c33e898218 |
@ -9,6 +9,7 @@ DB_ECHO=True
|
||||
# Системные переменные
|
||||
SECRET_KEY=1234567890abcdefghigklmnopqrstuvwxyz
|
||||
FRONTEND_URL=http://127.0.0.1:8000/api/v1
|
||||
ACCESS_TOKEN_EXPIRE=3600
|
||||
|
||||
# Переменные для почты
|
||||
EMAIL_HOST=smtp.yandex.ru
|
||||
|
@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install poetry==2.0.0
|
||||
pip install poetry==2.1.3
|
||||
poetry install
|
||||
poetry run pre-commit install
|
||||
|
||||
|
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install poetry==2.0.0
|
||||
pip install poetry==2.1.3
|
||||
poetry install
|
||||
poetry run pre-commit install
|
||||
|
||||
|
@ -49,3 +49,8 @@ repos:
|
||||
args: [ "--fix", "--line-length=120" ]
|
||||
- id: ruff-format
|
||||
args: [ "--line-length=120" ]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.15.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
|
27
Makefile
Normal file
27
Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
.PHONY: install create_migration_dev install_dev lint migrate run_dev run_prod
|
||||
|
||||
install:
|
||||
poetry install
|
||||
|
||||
install_dev: install
|
||||
poetry run pre-commit install
|
||||
|
||||
run_dev:
|
||||
poetry run app
|
||||
|
||||
run_prod: install migrate
|
||||
poetry run app
|
||||
|
||||
run_all:
|
||||
docker compose up -d
|
||||
$(MAKE) run_prod
|
||||
|
||||
migrate:
|
||||
poetry run alembic upgrade head
|
||||
|
||||
create_migration_dev:
|
||||
@read -p "Введите описание ревизии: " msg; \
|
||||
poetry run alembic revision --autogenerate -m "$$msg"
|
||||
|
||||
lint:
|
||||
poetry run pre-commit run --all
|
21
README.md
21
README.md
@ -1,5 +1,10 @@
|
||||
# Napkin Tools: Lkeep (Links Keeper)
|
||||
|
||||

|
||||
[](https://t.me/press_any_button)
|
||||
[](https://t.me/writeanynotes)
|
||||
[](https://t.me/+Li2vbxfWo0Q4ZDk6)
|
||||
|
||||
Lkeep — сервис сокращения ссылок, написанный на Python с использованием современных технологий, таких как FastAPI,
|
||||
PostgreSQL, Poetry, Pydantic и других.
|
||||
|
||||
@ -30,6 +35,10 @@ PostgreSQL, Poetry, Pydantic и других.
|
||||
- **smtplib** — стандартный модуль Python для отправки электронной почты через протокол SMTP.
|
||||
- **jinja2** — современный и гибкий шаблонизатор, который позволяет динамически генерировать HTML и другие текстовые
|
||||
форматы.
|
||||
- **pyJWT** — библиотека для создания, подписи и верификации JSON Web Tokens (JWT). Используется для генерации токенов
|
||||
доступа, проверки их
|
||||
целостности, срока действия и подписи, а также работы с закодированными данными (payload) в соответствии со
|
||||
стандартами JWT.
|
||||
|
||||
## Репозитории
|
||||
|
||||
@ -48,6 +57,9 @@ PostgreSQL, Poetry, Pydantic и других.
|
||||
5. [FastAPI 5. Приложение аутентификации и Pydantic схемы](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-5-prilozhenie-autentifikacii-i-pydantic-sh/)
|
||||
6. [FastAPI 6. Пользовательский сервис и маршруты регистрации](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-6-polzovatelskij-servis-i-marshruty-regist/)
|
||||
7. [FastAPI 7. Электронная почта, подтверждение регистрации, Celery и Redis](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-7-elektronnaya-pochta-podtverzhdenie-registracii-celery-i-redis/)
|
||||
8. [FastAPI 8. Маршрут авторизации и JWT](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-8-marshrut-avtorizacii-i-jwt/)
|
||||
9. [FastAPI 9. Logout и проверка авторизации](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-9-logout-i-proverka-avtorizacii/)
|
||||
10. [FastAPI 10. Изменение данных пользователя]()
|
||||
|
||||
## Установка
|
||||
|
||||
@ -92,7 +104,14 @@ PostgreSQL, Poetry, Pydantic и других.
|
||||
Затем откройте файл `.env` и заполните его значениями, соответствующими вашей системе (например, настройки
|
||||
подключения к базе данных PostgreSQL).
|
||||
|
||||
4. **Запустите приложение:**
|
||||
4. **Запустите БД и Redis**
|
||||
|
||||
Для запуска контейнера с PostgreSQL и Redis используйте команду в терминале:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Запустите приложение:**
|
||||
|
||||
Для запуска сервера в режиме разработки используйте команду с Poetry:
|
||||
```bash
|
||||
|
@ -3,7 +3,7 @@
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||
script_location = lkeep/database/alembic
|
||||
script_location = src/lkeep/database/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
|
39
docker-compose.dev.yaml
Normal file
39
docker-compose.dev.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
services:
|
||||
lkeep_db:
|
||||
image: postgres:17.5-alpine
|
||||
container_name: lkeep_db
|
||||
restart: always
|
||||
environment:
|
||||
- TZ=Europe/Moscow
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- lkeep_db:/var/lib/postgresql/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "sh -c 'pg_isready -U ${DB_USER} -d ${DB_NAME}'" ]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
lkeep_redis:
|
||||
image: redis:7.4-alpine
|
||||
container_name: lkeep_redis
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- lkeep_redis:/data
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
lkeep_db:
|
||||
lkeep_redis:
|
@ -1,28 +0,0 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from lkeep.core.settings import settings
|
||||
|
||||
|
||||
class AuthHandler:
|
||||
"""
|
||||
Обрабатывает аутентификационные запросы и обеспечивает безопасность пользовательских данных.
|
||||
|
||||
:ivar secret: Секретный ключ, используемый для дополнительной безопасности при генерации хешей.
|
||||
:type secret: str
|
||||
:ivar pwd_context: Контекст для использования bcrypt-алгоритма хеширования паролей.
|
||||
:type pwd_context: CryptContext
|
||||
"""
|
||||
|
||||
secret = settings.secret_key.get_secret_value()
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
async def get_password_hash(self, password: str) -> str:
|
||||
"""
|
||||
Генерирует хэш-значение пароля для безопасного сохранения и сравнения.
|
||||
|
||||
:param password: Пароль пользователя, который нужно зашифровать.
|
||||
:type password: str
|
||||
:returns: Хешированный вариант пароля.
|
||||
:rtype: str
|
||||
"""
|
||||
return self.pwd_context.hash(password)
|
@ -1,66 +0,0 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy import insert, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from lkeep.apps.auth.schemas import CreateUser, UserReturnData
|
||||
from lkeep.core.core_dependency.db_dependency import DBDependency
|
||||
from lkeep.database.models import User
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""
|
||||
Класс для управления пользователями.
|
||||
"""
|
||||
|
||||
def __init__(self, db: DBDependency = Depends(DBDependency)) -> None:
|
||||
"""
|
||||
Инициализирует экземпляр класса.
|
||||
|
||||
:param db: Зависимость от базы данных. По умолчанию используется Depends(DBDependency).
|
||||
:type db: DBDependency
|
||||
"""
|
||||
self.db = db
|
||||
self.model = User
|
||||
|
||||
async def create_user(self, user: CreateUser) -> UserReturnData:
|
||||
"""
|
||||
Создает нового пользователя в базе данных.
|
||||
|
||||
:param user: Объект с данными для создания пользователя.
|
||||
:type user: CreateUser
|
||||
:returns: Данные созданного пользователя.
|
||||
:rtype: UserReturnData
|
||||
:raises HTTPException: Если пользователь уже существует.
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = insert(self.model).values(**user.model_dump()).returning(self.model)
|
||||
|
||||
try:
|
||||
result = await session.execute(query)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status_code=400, detail="User already exists.")
|
||||
|
||||
await session.commit()
|
||||
|
||||
user_data = result.scalar_one()
|
||||
return UserReturnData(**user_data.__dict__)
|
||||
|
||||
async def confirm_user(self, email: str) -> None:
|
||||
"""
|
||||
Асинхронный метод для подтверждения пользователя по электронной почте.
|
||||
|
||||
:param email: Электронная почта пользователя, которого нужно подтвердить.
|
||||
:type email: str
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = update(self.model).where(self.model.email == email).values(is_verified=True, is_active=True)
|
||||
await session.execute(query)
|
||||
await session.commit()
|
@ -1,46 +0,0 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette import status
|
||||
|
||||
from lkeep.apps.auth.schemas import RegisterUser, UserReturnData
|
||||
from lkeep.apps.auth.services import UserService
|
||||
|
||||
auth_router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@auth_router.post(path="/register", response_model=UserReturnData, status_code=status.HTTP_201_CREATED)
|
||||
async def registration(user: RegisterUser, service: UserService = Depends(UserService)) -> UserReturnData:
|
||||
"""
|
||||
Регистрация нового пользователя.
|
||||
|
||||
:param user: Данные нового пользователя, который нужно зарегистрировать.
|
||||
:type user: RegisterUser
|
||||
:param service: Сервис для взаимодействия с пользователями.
|
||||
:type service: UserService
|
||||
:returns: Данные зарегистрированного пользователя.
|
||||
:rtype: UserReturnData
|
||||
:raises HTTPException 400: Если данные пользователя некорректны.
|
||||
"""
|
||||
return await service.register_user(user=user)
|
||||
|
||||
|
||||
@auth_router.get(path="/register_confirm", status_code=status.HTTP_200_OK)
|
||||
async def confirm_registration(token: str, service: UserService = Depends(UserService)) -> dict[str, str]:
|
||||
"""
|
||||
Подтверждает регистрацию пользователя по ссылке.
|
||||
|
||||
:param token: Токен подтверждения регистрации, полученный после отправки на электронную почту.
|
||||
:type token: str
|
||||
:raises HTTPException: Если токен недействителен или срок действия истек.
|
||||
:return: Словарь с сообщением о успешной подтверждении электронной почты.
|
||||
:rtype: dict[str, str]
|
||||
"""
|
||||
await service.confirm_user(token=token)
|
||||
return {"message": "Электронная почта подтверждена"}
|
874
poetry.lock
generated
874
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -22,14 +22,22 @@ dependencies = [
|
||||
"celery (>=5.4.0,<6.0.0)",
|
||||
"redis (>=5.2.1,<6.0.0)",
|
||||
"itsdangerous (>=2.2.0,<3.0.0)",
|
||||
"pyjwt (>=2.10.1,<3.0.0)",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{include = "lkeep", from = "src"}]
|
||||
|
||||
[project.scripts]
|
||||
app = "lkeep.main:start"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^25.1.0"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
indent-width = 4
|
||||
|
@ -9,7 +9,9 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lkeep.apps.auth.routes import auth_router
|
||||
from lkeep.apps.profile.routes import profile_router
|
||||
|
||||
apps_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
apps_router.include_router(router=auth_router)
|
||||
apps_router.include_router(router=profile_router)
|
52
src/lkeep/apps/auth/depends.py
Normal file
52
src/lkeep/apps/auth/depends.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from starlette import status
|
||||
|
||||
from lkeep.apps.auth.handlers import AuthHandler
|
||||
from lkeep.apps.auth.managers import UserManager
|
||||
from lkeep.apps.auth.schemas import UserVerifySchema
|
||||
from lkeep.apps.auth.utils import get_token_from_cookies
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(get_token_from_cookies)],
|
||||
handler: AuthHandler = Depends(AuthHandler),
|
||||
manager: UserManager = Depends(UserManager),
|
||||
) -> UserVerifySchema:
|
||||
"""
|
||||
Получает текущего пользователя из токена аутентификации.
|
||||
|
||||
:param token: Токен аутентификации, полученный из куки.
|
||||
:type token: str
|
||||
:param handler: Обработчик аутентификации, использующийся для декодирования токена.
|
||||
:type handler: AuthHandler
|
||||
:param manager: Менеджер пользователей, используется для проверки и получения данных о пользователе.
|
||||
:type manager: UserManager
|
||||
:returns: Схема с информацией о текущем пользователе.
|
||||
:rtype: UserVerifySchema
|
||||
:raises HTTPException: Если токен невалиден или пользователь не найден.
|
||||
"""
|
||||
decoded_token = await handler.decode_access_token(token=token)
|
||||
user_id = str(decoded_token.get("user_id"))
|
||||
session_id = str(decoded_token.get("session_id"))
|
||||
|
||||
if not await manager.get_access_token(user_id=user_id, session_id=session_id):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token is invalid")
|
||||
|
||||
user = await manager.get_user_by_id(user_id=uuid.UUID(user_id))
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
|
||||
user.session_id = session_id
|
||||
|
||||
return user
|
93
src/lkeep/apps/auth/handlers.py
Normal file
93
src/lkeep/apps/auth/handlers.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException
|
||||
from passlib.context import CryptContext
|
||||
from starlette import status
|
||||
|
||||
from lkeep.apps.auth.named_tuples import CreateTokenTuple
|
||||
from lkeep.core.settings import settings
|
||||
|
||||
|
||||
class AuthHandler:
|
||||
"""
|
||||
Обрабатывает аутентификационные запросы и обеспечивает безопасность пользовательских данных.
|
||||
|
||||
:ivar secret: Секретный ключ, используемый для дополнительной безопасности при генерации хешей.
|
||||
:type secret: str
|
||||
:ivar pwd_context: Контекст для использования bcrypt-алгоритма хеширования паролей.
|
||||
:type pwd_context: CryptContext
|
||||
"""
|
||||
|
||||
secret = settings.secret_key.get_secret_value()
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
async def get_password_hash(self, password: str) -> str:
|
||||
"""
|
||||
Генерирует хэш-значение пароля для безопасного сохранения и сравнения.
|
||||
|
||||
:param password: Пароль пользователя, который нужно зашифровать.
|
||||
:type password: str
|
||||
:returns: Хешированный вариант пароля.
|
||||
:rtype: str
|
||||
"""
|
||||
return self.pwd_context.hash(password)
|
||||
|
||||
async def verify_password(self, raw_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
Проверяет соответствие введенного пароля захэшированному паролю.
|
||||
|
||||
:param raw_password: Введенный пользователем пароль.
|
||||
:type raw_password: str
|
||||
:param hashed_password: Хэш, с которым сравнивается введенный пароль.
|
||||
:type hashed_password: str
|
||||
:returns: Логическое значение, указывающее на успешность проверки.
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.pwd_context.verify(raw_password, hashed_password)
|
||||
|
||||
async def create_access_token(self, user_id: uuid.UUID | str) -> CreateTokenTuple:
|
||||
"""
|
||||
Создаёт JWT-токен доступа для пользователя.
|
||||
|
||||
:param user_id: Уникальный идентификатор пользователя (UUID).
|
||||
:type user_id: uuid.UUID
|
||||
:returns: Кортеж, содержащий закодированный JWT-токен и уникальный session_id.
|
||||
:rtype: CreateTokenTuple
|
||||
"""
|
||||
expire = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=settings.access_token_expire)
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
data = {"exp": expire, "session_id": session_id, "user_id": str(user_id)}
|
||||
|
||||
encoded_jwt = jwt.encode(payload=data, key=self.secret, algorithm="HS256")
|
||||
|
||||
return CreateTokenTuple(encoded_jwt=encoded_jwt, session_id=session_id)
|
||||
|
||||
async def decode_access_token(self, token: str) -> dict:
|
||||
"""
|
||||
Декодирует JWT-токен и возвращает его содержимое.
|
||||
|
||||
:param token: Строка с JWT-токеном, который нужно декодировать.
|
||||
:type token: str
|
||||
:returns: Данные, содержащиеся в декодированном токене.
|
||||
:rtype: dict
|
||||
:raises HTTPException: При ошибке декодирования токена (например, токен просрочен или невалиден).
|
||||
Статус-код ответа 401 UNAUTHORIZED, детализация "Token has expired" при просрочке,
|
||||
и "Invalid token" при недопустимости токена.
|
||||
"""
|
||||
try:
|
||||
return jwt.decode(jwt=token, key=self.secret, algorithms=["HS256"])
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
161
src/lkeep/apps/auth/managers.py
Normal file
161
src/lkeep/apps/auth/managers.py
Normal file
@ -0,0 +1,161 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy import insert, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from lkeep.apps.auth.schemas import (
|
||||
CreateUser,
|
||||
GetUserWithIDAndEmail,
|
||||
UserReturnData,
|
||||
UserVerifySchema,
|
||||
)
|
||||
from lkeep.core.core_dependency.db_dependency import DBDependency
|
||||
from lkeep.core.core_dependency.redis_dependency import RedisDependency
|
||||
from lkeep.database.models import User
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""
|
||||
Класс для управления пользователями.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, db: DBDependency = Depends(DBDependency), redis: RedisDependency = Depends(RedisDependency)
|
||||
) -> None:
|
||||
"""
|
||||
Инициализирует экземпляр класса.
|
||||
|
||||
:param db: Зависимость для базы данных. По умолчанию используется Depends(DBDependency).
|
||||
:type db: DBDependency
|
||||
:param redis: Зависимость для Redis. По умолчанию используется Depends(RedisDependency).
|
||||
:type redis: RedisDependency
|
||||
"""
|
||||
self.db = db
|
||||
self.model = User
|
||||
self.redis = redis
|
||||
|
||||
async def create_user(self, user: CreateUser) -> UserReturnData:
|
||||
"""
|
||||
Создает нового пользователя в базе данных.
|
||||
|
||||
:param user: Объект с данными для создания пользователя.
|
||||
:type user: CreateUser
|
||||
:returns: Данные созданного пользователя.
|
||||
:rtype: UserReturnData
|
||||
:raises HTTPException: Если пользователь уже существует.
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = insert(self.model).values(**user.model_dump()).returning(self.model)
|
||||
|
||||
try:
|
||||
result = await session.execute(query)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status_code=400, detail="User already exists.")
|
||||
|
||||
await session.commit()
|
||||
|
||||
user_data = result.scalar_one()
|
||||
return UserReturnData(**user_data.__dict__)
|
||||
|
||||
async def confirm_user(self, email: str) -> None:
|
||||
"""
|
||||
Асинхронный метод для подтверждения пользователя по электронной почте.
|
||||
|
||||
:param email: Электронная почта пользователя, которого нужно подтвердить.
|
||||
:type email: str
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = update(self.model).where(self.model.email == email).values(is_verified=True, is_active=True)
|
||||
await session.execute(query)
|
||||
await session.commit()
|
||||
|
||||
async def get_user_by_email(self, email: str) -> GetUserWithIDAndEmail | None:
|
||||
"""
|
||||
Возвращает пользователя по указанному адресу электронной почты.
|
||||
|
||||
:param email: Адрес электронной почты пользователя для поиска.
|
||||
:type email: str
|
||||
:return: Объект пользователя с полями id и email, если пользователь найден; None в противном случае.
|
||||
:rtype: GetUserWithIDAndEmail | None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.model.id, self.model.email, self.model.hashed_password).where(self.model.email == email)
|
||||
|
||||
result = await session.execute(query)
|
||||
user = result.mappings().first()
|
||||
|
||||
if user:
|
||||
return GetUserWithIDAndEmail(**user)
|
||||
|
||||
return None
|
||||
|
||||
async def get_user_by_id(self, user_id: uuid.UUID | str) -> UserVerifySchema | None:
|
||||
"""
|
||||
Возвращает информацию о пользователе по его идентификатору.
|
||||
|
||||
:param user_id: Идентификатор пользователя, для которого нужно получить информацию.
|
||||
Может быть представлен в виде UUID или строки.
|
||||
:type user_id: uuid.UUID | str
|
||||
:returns: Схема данных пользователя, если пользователь найден; None, если пользователь не найден.
|
||||
:rtype: UserVerifySchema | None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.model.id, self.model.email).where(self.model.id == user_id)
|
||||
|
||||
result = await session.execute(query)
|
||||
user = result.mappings().one_or_none()
|
||||
|
||||
if user:
|
||||
return UserVerifySchema(**user)
|
||||
|
||||
return None
|
||||
|
||||
async def store_access_token(self, token: str, user_id: uuid.UUID | str, session_id: str) -> None:
|
||||
"""
|
||||
Сохраняет токен доступа в хранилище (Redis).
|
||||
|
||||
:param token: Токен доступа для сохранения.
|
||||
:type token: str
|
||||
:param user_id: Идентификатор пользователя, которому принадлежит токен.
|
||||
:type user_id: uuid.UUID
|
||||
:param session_id: Идентификатор сессии, связанной с токеном.
|
||||
:type session_id: str
|
||||
"""
|
||||
async with self.redis.get_client() as client:
|
||||
await client.set(f"{user_id}:{session_id}", token)
|
||||
|
||||
async def get_access_token(self, user_id: uuid.UUID | str, session_id: str) -> str | None:
|
||||
"""
|
||||
Получает токен доступа из кэша по идентификаторам пользователя и сессии.
|
||||
|
||||
:param user_id: Идентификатор пользователя, может быть в формате UUID или строка.
|
||||
:type user_id: uuid.UUID | str
|
||||
:param session_id: Идентификатор сессии, строка.
|
||||
:type session_id: str
|
||||
:returns: Токен доступа из кэша, если он существует; иначе None.
|
||||
:rtype: str | None
|
||||
"""
|
||||
async with self.redis.get_client() as client:
|
||||
return await client.get(f"{user_id}:{session_id}")
|
||||
|
||||
async def revoke_access_token(self, user_id: uuid.UUID | str, session_id: str | uuid.UUID | None) -> None:
|
||||
"""
|
||||
Отзывает доступный токен доступа пользователя.
|
||||
|
||||
:param user_id: Идентификатор пользователя, которому принадлежит токен.
|
||||
:type user_id: uuid.UUID | str
|
||||
:param session_id: Идентификатор сессии, для которой должен быть отозван токен.
|
||||
Если None, то все сессии пользователя будут отозваны.
|
||||
:type session_id: str | uuid.UUID | None
|
||||
"""
|
||||
async with self.redis.get_client() as client:
|
||||
await client.delete(f"{user_id}:{session_id}")
|
27
src/lkeep/apps/auth/named_tuples.py
Normal file
27
src/lkeep/apps/auth/named_tuples.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class CreateTokenTuple(NamedTuple):
|
||||
"""
|
||||
Класс для создания кортежа токенов, содержащего закодированный JWT и идентификатор сессии.
|
||||
|
||||
Класс наследует от `NamedTuple` и представляет собой неизменяемый контейнер для хранения двух значений:
|
||||
- закодированного JSON Web Token (JWT)
|
||||
- уникального идентификатора сессии.
|
||||
|
||||
:ivar encoded_jwt: Закодированный JWT-токен.
|
||||
:type encoded_jwt: str
|
||||
:ivar session_id: Уникальный идентификатор сессии.
|
||||
:type session_id: str
|
||||
"""
|
||||
|
||||
encoded_jwt: str
|
||||
session_id: str
|
100
src/lkeep/apps/auth/routes.py
Normal file
100
src/lkeep/apps/auth/routes.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette import status
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lkeep.apps.auth.depends import get_current_user
|
||||
from lkeep.apps.auth.schemas import AuthUser, UserReturnData, UserVerifySchema
|
||||
from lkeep.apps.auth.services import UserService
|
||||
|
||||
auth_router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@auth_router.post(path="/register", response_model=UserReturnData, status_code=status.HTTP_201_CREATED)
|
||||
async def registration(user: AuthUser, service: UserService = Depends(UserService)) -> UserReturnData:
|
||||
"""
|
||||
Регистрация нового пользователя.
|
||||
|
||||
:param user: Данные нового пользователя, который нужно зарегистрировать.
|
||||
:type user: AuthUser
|
||||
:param service: Сервис для взаимодействия с пользователями.
|
||||
:type service: UserService
|
||||
:returns: Данные зарегистрированного пользователя.
|
||||
:rtype: UserReturnData
|
||||
:raises HTTPException 400: Если данные пользователя некорректны.
|
||||
"""
|
||||
return await service.register_user(user=user)
|
||||
|
||||
|
||||
@auth_router.get(path="/register_confirm", status_code=status.HTTP_200_OK)
|
||||
async def confirm_registration(token: str, service: UserService = Depends(UserService)) -> dict[str, str]:
|
||||
"""
|
||||
Подтверждает регистрацию пользователя по ссылке.
|
||||
|
||||
:param token: Токен подтверждения регистрации, полученный после отправки на электронную почту.
|
||||
:type token: str
|
||||
:param service: Сервис для взаимодействия с пользователями.
|
||||
:raises HTTPException: Если токен недействителен или срок действия истек.
|
||||
:return: Словарь с сообщением о успешной подтверждении электронной почты.
|
||||
:rtype: dict[str, str]
|
||||
"""
|
||||
await service.confirm_user(token=token)
|
||||
|
||||
return {"message": "Электронная почта подтверждена"}
|
||||
|
||||
|
||||
@auth_router.post(path="/login", status_code=status.HTTP_200_OK)
|
||||
async def login(user: AuthUser, service: UserService = Depends(UserService)) -> JSONResponse:
|
||||
"""
|
||||
Вход пользователя в систему.
|
||||
|
||||
:param user: Объект данных пользователя для входа.
|
||||
:type user: AuthUser
|
||||
:param service: Сервисный объект для управления пользователями.
|
||||
:type service: UserService
|
||||
:returns: JSON-ответ с токеном доступа в Cookies, если вход выполнен успешно.
|
||||
:rtype: JSONResponse
|
||||
:raises HTTPException: Если учетные данные не верны или произошла другая ошибка при входе.
|
||||
"""
|
||||
return await service.login_user(user=user)
|
||||
|
||||
|
||||
@auth_router.get(path="/logout", status_code=status.HTTP_200_OK)
|
||||
async def logout(
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)], service: UserService = Depends(UserService)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Описание функции logout.
|
||||
|
||||
:param user: Текущий авторизованный пользователь.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервис для управления пользователями.
|
||||
:type service: UserService
|
||||
:returns: JSON-ответ, содержащий результат логаута.
|
||||
:rtype: JSONResponse
|
||||
"""
|
||||
return await service.logout_user(user=user)
|
||||
|
||||
|
||||
@auth_router.get(path="/get-user", status_code=status.HTTP_200_OK, response_model=UserVerifySchema)
|
||||
async def get_auth_user(user: Annotated[UserVerifySchema, Depends(get_current_user)]) -> UserVerifySchema:
|
||||
"""
|
||||
Возвращает информацию об авторизованном пользователе.
|
||||
|
||||
:param user: Информация о пользователе, полученная с помощью механизма аутентификации.
|
||||
:type user: UserVerifySchema
|
||||
:return: Схема данных пользователя, содержащая необходимую информацию для работы системы.
|
||||
:rtype: UserVerifySchema
|
||||
|
||||
:raises HTTPException 401: Если пользователь не авторизован и попытка получить доступ к защищенному ресурсу.
|
||||
"""
|
||||
return user
|
@ -8,8 +8,9 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel, EmailStr, StringConstraints
|
||||
|
||||
|
||||
class GetUserByID(BaseModel):
|
||||
@ -34,7 +35,18 @@ class GetUserByEmail(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class RegisterUser(GetUserByEmail):
|
||||
class UserVerifySchema(GetUserByID, GetUserByEmail):
|
||||
"""
|
||||
Класс для валидации данных пользователя.
|
||||
|
||||
Данный класс наследует методы из классов GetUserByID и GetUserByEmail,
|
||||
что позволяет использовать их функциональность для проверки данных пользователя.
|
||||
"""
|
||||
|
||||
session_id: uuid.UUID | str | None = None
|
||||
|
||||
|
||||
class AuthUser(GetUserByEmail):
|
||||
"""
|
||||
Класс для регистрации пользователя, наследующий класс GetUserByEmail.
|
||||
|
||||
@ -42,7 +54,7 @@ class RegisterUser(GetUserByEmail):
|
||||
:type password: str
|
||||
"""
|
||||
|
||||
password: str
|
||||
password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
||||
|
||||
|
||||
class CreateUser(GetUserByEmail):
|
||||
@ -56,6 +68,21 @@ class CreateUser(GetUserByEmail):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class GetUserWithIDAndEmail(GetUserByID, CreateUser):
|
||||
"""
|
||||
Класс для получения пользователя по его ID и email.
|
||||
|
||||
:ivar id: Уникальный идентификатор пользователя.
|
||||
:type id: int
|
||||
:ivar email: Адрес электронной почты пользователя.
|
||||
:type email: str
|
||||
:ivar hashed_password: Хэшированный пароль пользователя.
|
||||
:type hashed_password: str
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserReturnData(GetUserByID, GetUserByEmail):
|
||||
"""
|
||||
Класс для представления данных пользователя, возвращаемых из API.
|
@ -8,10 +8,17 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from itsdangerous import BadSignature, URLSafeTimedSerializer
|
||||
from starlette import status
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lkeep.apps.auth.handlers import AuthHandler
|
||||
from lkeep.apps.auth.managers import UserManager
|
||||
from lkeep.apps.auth.schemas import CreateUser, RegisterUser, UserReturnData
|
||||
from lkeep.apps.auth.schemas import (
|
||||
AuthUser,
|
||||
CreateUser,
|
||||
UserReturnData,
|
||||
UserVerifySchema,
|
||||
)
|
||||
from lkeep.apps.auth.tasks import send_confirmation_email
|
||||
from lkeep.core.settings import settings
|
||||
|
||||
@ -36,12 +43,12 @@ class UserService:
|
||||
self.handler = handler
|
||||
self.serializer = URLSafeTimedSerializer(secret_key=settings.secret_key.get_secret_value())
|
||||
|
||||
async def register_user(self, user: RegisterUser) -> UserReturnData:
|
||||
async def register_user(self, user: AuthUser) -> UserReturnData:
|
||||
"""
|
||||
Регистрирует нового пользователя в системе.
|
||||
|
||||
:param user: Информация о пользователе, который нужно зарегистрировать.
|
||||
:type user: RegisterUser
|
||||
:type user: AuthUser
|
||||
:returns: Данные о созданном пользователе.
|
||||
:rtype: UserReturnData
|
||||
"""
|
||||
@ -70,3 +77,51 @@ class UserService:
|
||||
raise HTTPException(status_code=400, detail="Неверный или просроченный токен")
|
||||
|
||||
await self.manager.confirm_user(email=email)
|
||||
|
||||
async def login_user(self, user: AuthUser) -> JSONResponse:
|
||||
"""
|
||||
Вход пользователя в систему.
|
||||
|
||||
:param user: Объект пользователя с входными данными для аутентификации.
|
||||
:type user: AuthUser
|
||||
:returns: Ответ сервера, указывающий на успешность или неудачу входа.
|
||||
:rtype: JSONResponse
|
||||
:raises HTTPException: Если предоставленные учетные данные неверны (HTTP 401 Unauthorized).
|
||||
"""
|
||||
exist_user = await self.manager.get_user_by_email(email=user.email)
|
||||
|
||||
if exist_user is None or not await self.handler.verify_password(
|
||||
hashed_password=exist_user.hashed_password, raw_password=user.password
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong email or password")
|
||||
|
||||
token, session_id = await self.handler.create_access_token(user_id=exist_user.id)
|
||||
|
||||
await self.manager.store_access_token(token=token, user_id=exist_user.id, session_id=session_id)
|
||||
|
||||
response = JSONResponse(content={"message": "Вход успешен"})
|
||||
response.set_cookie(
|
||||
key="Authorization",
|
||||
value=token,
|
||||
httponly=True,
|
||||
max_age=settings.access_token_expire,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def logout_user(self, user: UserVerifySchema) -> JSONResponse:
|
||||
"""
|
||||
Отправляет запрос на выход пользователя из системы.
|
||||
|
||||
:param user: Схема, содержащая информацию о пользователе для аутентификации.
|
||||
:type user: UserVerifySchema
|
||||
:returns: Ответ сервера с сообщением об успешном выходе пользователя.
|
||||
:rtype: JSONResponse
|
||||
:raises Exception: Если произошла ошибка при отмене токена доступа.
|
||||
"""
|
||||
await self.manager.revoke_access_token(user_id=user.id, session_id=user.session_id)
|
||||
|
||||
response = JSONResponse(content={"message": "Logged out"})
|
||||
response.delete_cookie(key="Authorization")
|
||||
|
||||
return response
|
27
src/lkeep/apps/auth/utils.py
Normal file
27
src/lkeep/apps/auth/utils.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
async def get_token_from_cookies(request: Request) -> str:
|
||||
"""
|
||||
Получает токен из куки запроса.
|
||||
|
||||
:param request: Объект HTTP-запроса.
|
||||
:type request: Request
|
||||
:return: Токен из cookies.
|
||||
:rtype: str
|
||||
:raises HTTPException: Если в запросе отсутствует cookie с ключом "Authorization".
|
||||
"""
|
||||
token = request.cookies.get("Authorization")
|
||||
if token is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token is missing")
|
||||
return token
|
30
src/lkeep/apps/profile/managers.py
Normal file
30
src/lkeep/apps/profile/managers.py
Normal file
@ -0,0 +1,30 @@
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from lkeep.core.core_dependency.db_dependency import DBDependency
|
||||
from lkeep.database.models import User
|
||||
|
||||
|
||||
class ProfileManager:
|
||||
def __init__(self, db: DBDependency = Depends(DBDependency)) -> None:
|
||||
self.db = db
|
||||
self.user_model = User
|
||||
|
||||
async def update_user_fields(self, user_id: uuid.UUID | str, **kwargs: Any) -> None:
|
||||
async with self.db.db_session() as session:
|
||||
query = update(self.user_model).where(self.user_model.id == user_id).values(**kwargs)
|
||||
|
||||
await session.execute(query)
|
||||
|
||||
await session.commit()
|
||||
|
||||
async def get_user_hashed_password(self, user_id: uuid.UUID | str) -> str:
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.user_model.hashed_password).where(self.user_model.id == user_id)
|
||||
|
||||
result = await session.execute(query)
|
||||
|
||||
return result.scalar()
|
30
src/lkeep/apps/profile/routes.py
Normal file
30
src/lkeep/apps/profile/routes.py
Normal file
@ -0,0 +1,30 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette import status
|
||||
from starlette.responses import Response
|
||||
|
||||
from lkeep.apps.auth.depends import get_current_user
|
||||
from lkeep.apps.auth.schemas import UserVerifySchema
|
||||
from lkeep.apps.profile.schemas import ChangeEmailRequest, ChangePasswordRequest
|
||||
from lkeep.apps.profile.services import ProfileService
|
||||
|
||||
profile_router = APIRouter(prefix="/profile", tags=["profile"])
|
||||
|
||||
|
||||
@profile_router.post("/change-email", status_code=status.HTTP_200_OK)
|
||||
async def change_email(
|
||||
data: ChangeEmailRequest,
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: ProfileService = Depends(ProfileService),
|
||||
) -> None:
|
||||
return await service.change_email(data=data, user=user)
|
||||
|
||||
|
||||
@profile_router.post("/change-password", status_code=status.HTTP_200_OK)
|
||||
async def change_password(
|
||||
data: ChangePasswordRequest,
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: ProfileService = Depends(ProfileService),
|
||||
) -> Response:
|
||||
return await service.change_password(data=data, user=user)
|
12
src/lkeep/apps/profile/schemas.py
Normal file
12
src/lkeep/apps/profile/schemas.py
Normal file
@ -0,0 +1,12 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, EmailStr, StringConstraints
|
||||
|
||||
|
||||
class ChangeEmailRequest(BaseModel):
|
||||
new_email: EmailStr
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
||||
new_password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
30
src/lkeep/apps/profile/services.py
Normal file
30
src/lkeep/apps/profile/services.py
Normal file
@ -0,0 +1,30 @@
|
||||
from fastapi import Depends
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lkeep.apps.auth.handlers import AuthHandler
|
||||
from lkeep.apps.auth.schemas import UserVerifySchema
|
||||
from lkeep.apps.profile.managers import ProfileManager
|
||||
from lkeep.apps.profile.schemas import ChangeEmailRequest, ChangePasswordRequest
|
||||
|
||||
|
||||
class ProfileService:
|
||||
def __init__(
|
||||
self,
|
||||
manager: ProfileManager = Depends(ProfileManager),
|
||||
handler: AuthHandler = Depends(AuthHandler),
|
||||
) -> None:
|
||||
self.manager = manager
|
||||
self.handler = handler
|
||||
|
||||
async def change_email(self, data: ChangeEmailRequest, user: UserVerifySchema) -> None:
|
||||
return await self.manager.update_user_fields(user_id=user.id, email=data.new_email)
|
||||
|
||||
async def change_password(self, data: ChangePasswordRequest, user: UserVerifySchema) -> None | JSONResponse:
|
||||
current_password_hash = await self.manager.get_user_hashed_password(user_id=user.id)
|
||||
|
||||
if await self.handler.verify_password(raw_password=data.old_password, hashed_password=current_password_hash):
|
||||
hashed_password = await self.handler.get_password_hash(password=data.new_password)
|
||||
await self.manager.update_user_fields(user_id=user.id, hashed_password=hashed_password)
|
||||
return None
|
||||
|
||||
return JSONResponse({"error": "Invalid password"}, status_code=401)
|
0
src/lkeep/core/core_dependency/__init__.py
Normal file
0
src/lkeep/core/core_dependency/__init__.py
Normal file
55
src/lkeep/core/core_dependency/redis_dependency.py
Normal file
55
src/lkeep/core/core_dependency/redis_dependency.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from redis.asyncio import ConnectionPool, Redis
|
||||
|
||||
from lkeep.core.settings import settings
|
||||
|
||||
|
||||
class RedisDependency:
|
||||
"""
|
||||
Класс, предоставляющий инструменты для работы с Redis через асинхронный клиент.
|
||||
|
||||
:ivar _url: URL подключения к Redis серверу.
|
||||
:type _url: str
|
||||
:ivar _pool: Пул соединений для управления соединениями с Redis.
|
||||
:type _pool: ConnectionPool
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Инициализирует экземпляр класса для работы с Redis.
|
||||
"""
|
||||
self._url = settings.redis_settings.redis_url
|
||||
self._pool: ConnectionPool = self._init_pool()
|
||||
|
||||
def _init_pool(self) -> ConnectionPool:
|
||||
"""
|
||||
Инициализирует пул соединений Redis.
|
||||
|
||||
:returns: Пул соединений для работы с Redis.
|
||||
:rtype: ConnectionPool
|
||||
"""
|
||||
return ConnectionPool.from_url(url=self._url, encoding="utf-8", decode_responses=True)
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_client(self) -> AsyncGenerator[Redis, None]:
|
||||
"""
|
||||
Получает клиентскую сессию Redis для взаимодействия с базой данных.
|
||||
|
||||
:returns: Асинхронный генератор клиента Redis.
|
||||
:rtype: AsyncGenerator[Redis, None]
|
||||
"""
|
||||
redis_client = Redis(connection_pool=self._pool)
|
||||
try:
|
||||
yield redis_client
|
||||
finally:
|
||||
await redis_client.aclose()
|
@ -117,6 +117,8 @@ class Settings(BaseSettings):
|
||||
:type templates_dir: str
|
||||
:ivar frontend_url: Адрес фронтенд-приложения.
|
||||
:type frontend_url: str
|
||||
:ivar access_token_expire: Срок жизни JWT-токена
|
||||
:type access_token_expire: int
|
||||
"""
|
||||
|
||||
db_settings: DBSettings = DBSettings()
|
||||
@ -125,6 +127,8 @@ class Settings(BaseSettings):
|
||||
secret_key: SecretStr
|
||||
templates_dir: str = "templates"
|
||||
frontend_url: str
|
||||
access_token_expire: int
|
||||
domain: str
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore")
|
||||
|
0
src/lkeep/database/alembic/__init__.py
Normal file
0
src/lkeep/database/alembic/__init__.py
Normal file
0
src/lkeep/database/alembic/versions/__init__.py
Normal file
0
src/lkeep/database/alembic/versions/__init__.py
Normal file
@ -8,6 +8,7 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from lkeep.apps import apps_router
|
||||
|
||||
@ -15,6 +16,14 @@ app = FastAPI()
|
||||
|
||||
app.include_router(router=apps_router)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://127.0.0.1:3000"], # TODO: ЗАМЕНИТЬ ПОТОМ НА ДОМЕН
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def start():
|
||||
uvicorn.run(app="lkeep.main:app", reload=True)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Reference in New Issue
Block a user