- Добавлен новый маршрут смены почты и связанная с ним бизнес-логика - Добавлен новый маршрут смены пароля и связанная с ним бизнес логика - В схеме `AuthUser` изменена аннотация типа с `str` на `StringConstraints` с учётом минимальной и максимальной длинны пароля - В `alembic.ini` исправлен путь до директории с миграциями после последнего обновления архитектуры под `Poetry 2.1.3` - Добавлен `docker-compose.dev.yaml` для запуска БД и Redis в окружении для разработки - Добавлен `Makefile` с описанием основных команд - Обновлён `README.md`
This commit is contained in:
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
|
16
README.md
16
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 и других.
|
||||
|
||||
@ -53,6 +58,8 @@ PostgreSQL, Poetry, Pydantic и других.
|
||||
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. Изменение данных пользователя]()
|
||||
|
||||
## Установка
|
||||
|
||||
@ -97,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:
|
@ -37,3 +37,7 @@ 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)
|
||||
|
@ -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):
|
||||
@ -53,7 +54,7 @@ class AuthUser(GetUserByEmail):
|
||||
:type password: str
|
||||
"""
|
||||
|
||||
password: str
|
||||
password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
||||
|
||||
|
||||
class CreateUser(GetUserByEmail):
|
||||
|
0
src/lkeep/apps/profile/__init__.py
Normal file
0
src/lkeep/apps/profile/__init__.py
Normal file
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)
|
Reference in New Issue
Block a user