From d02ca36777afe0aa03325f837e5c0948781233d2 Mon Sep 17 00:00:00 2001 From: VinokurovVE Date: Thu, 1 Aug 2024 01:21:32 +0900 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20crud=20=D0=B8=20=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B2=20RESTfull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-app/api/__init__.py | 9 +++ backend-app/api/another_router.py | 7 --- backend-app/api/api_v1/__init__.py | 13 ++++ backend-app/api/api_v1/login.py | 53 +++++++++++++++++ backend-app/api/api_v1/user.py | 57 ++++++++++++++++++ backend-app/api/main_router.py | 7 --- backend-app/core/settings.py | 10 ++++ backend-app/main.py | 14 +---- .../repo/{crud.py => crud/__init__.py} | 0 backend-app/repo/crud/login.py | 58 ++++++++++++++++++ backend-app/repo/crud/user.py | 58 ++++++++++++++++++ backend-app/repo/schemas.py | 33 ++++++++++- backend-app/utils/hashing.py | 9 +++ poetry.lock | 59 ++++++++++++++++++- pyproject.toml | 2 + 15 files changed, 361 insertions(+), 28 deletions(-) delete mode 100644 backend-app/api/another_router.py create mode 100644 backend-app/api/api_v1/__init__.py create mode 100644 backend-app/api/api_v1/login.py create mode 100644 backend-app/api/api_v1/user.py delete mode 100644 backend-app/api/main_router.py rename backend-app/repo/{crud.py => crud/__init__.py} (100%) create mode 100644 backend-app/repo/crud/login.py create mode 100644 backend-app/repo/crud/user.py create mode 100644 backend-app/utils/hashing.py diff --git a/backend-app/api/__init__.py b/backend-app/api/__init__.py index e69de29..af48ebf 100644 --- a/backend-app/api/__init__.py +++ b/backend-app/api/__init__.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from core.settings import settings +from .api_v1 import router as v1_router +router = APIRouter( + prefix= settings.api.prefix, + tags=["Основной роутер"] +) + +router.include_router(v1_router) \ No newline at end of file diff --git a/backend-app/api/another_router.py b/backend-app/api/another_router.py deleted file mode 100644 index 468c5ff..0000000 --- a/backend-app/api/another_router.py +++ /dev/null @@ -1,7 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - -@router.get("/hello") -async def say_world(): - return {"msg":"Hello WORLD!"} \ No newline at end of file diff --git a/backend-app/api/api_v1/__init__.py b/backend-app/api/api_v1/__init__.py new file mode 100644 index 0000000..d89469d --- /dev/null +++ b/backend-app/api/api_v1/__init__.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter +from core.settings import settings +from .login import router as login_router +from .user import router as user_router +router = APIRouter( + prefix=settings.api.v1.prefix, +) +router.include_router( + user_router +) +router.include_router( + login_router +) \ No newline at end of file diff --git a/backend-app/api/api_v1/login.py b/backend-app/api/api_v1/login.py new file mode 100644 index 0000000..57013ee --- /dev/null +++ b/backend-app/api/api_v1/login.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, Depends +from core.settings import settings +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Annotated +from core.models import db_helper +from repo.schemas import LoginCreate,LoginRead, LoginUpdate +import repo.crud.login as crud +router = APIRouter( + prefix=settings.api.v1.login, + tags=["Login"] +) + +@router.get("/", response_model=list[LoginRead]) +async def get_all_logins( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)] + ): + logins = await crud.get_all_logins(session=session) + return logins + +@router.get("/{login_id}", response_model=LoginRead) +async def get_login( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + login_id: int + ): + login = await crud.get_login( + session=session, + login_id=login_id + ) + return login + +@router.post("/", response_model=LoginRead) +async def create_login( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + login_create: LoginCreate + ): + login = await crud.create_login( + session=session, + login_create=login_create + ) + return login + +@router.patch("/{login_id}", response_model=LoginRead) +async def update_login( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + login_id: int, + login_update: LoginUpdate + ): + login = await crud.update_login( + session=session, + login_id=login_id, + login_update=login_update + ) + return login diff --git a/backend-app/api/api_v1/user.py b/backend-app/api/api_v1/user.py new file mode 100644 index 0000000..c40315d --- /dev/null +++ b/backend-app/api/api_v1/user.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, Depends +from core.settings import settings +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Annotated +from repo.schemas import UserRead, UserCreate, UserUpdate +from core.models import db_helper + +import repo.crud.user as crud + +router = APIRouter( + prefix=settings.api.v1.user, + tags=["Users"] +) + + + +@router.get("/", response_model=list[UserRead]) +async def get_all_users( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)] + ): + users = await crud.get_all_users(session=session) + return users + +@router.get("/{user_id}", response_model=UserRead) +async def get_user( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + user_id: int + ): + users = await crud.get_user( + session=session, + user_id=user_id + ) + return users + +@router.post("/", response_model=UserRead) +async def create_user( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + user_create: UserCreate + ): + users = await crud.create_user( + session=session, + user_create=user_create + ) + return users + +@router.patch("/{user_id}", response_model=UserRead) +async def update_user( + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], + user_id: int, + user_update: UserUpdate + ): + users = await crud.update_user( + session=session, + user_id=user_id, + user_update=user_update + ) + return users diff --git a/backend-app/api/main_router.py b/backend-app/api/main_router.py deleted file mode 100644 index 1f7b835..0000000 --- a/backend-app/api/main_router.py +++ /dev/null @@ -1,7 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - -@router.get("/hello") -async def say_hello(): - return {"msg":"hello"} \ No newline at end of file diff --git a/backend-app/core/settings.py b/backend-app/core/settings.py index c3607e8..e6c56f2 100644 --- a/backend-app/core/settings.py +++ b/backend-app/core/settings.py @@ -5,6 +5,15 @@ class RunSettings(BaseModel): host: str = "0.0.0.0" port: int = 8000 +class APIV1Prefix(BaseModel): + prefix:str = "/v1" + user:str = "/user" + login:str = "/login" + +class APIPrefix(BaseModel): + prefix:str = "/api" + v1: APIV1Prefix = APIV1Prefix() + class DatabaseConfig(BaseModel): url: str echo:bool=False @@ -28,5 +37,6 @@ class Settings(BaseSettings): ) run: RunSettings = RunSettings() db: DatabaseConfig + api: APIPrefix = APIPrefix() settings = Settings() diff --git a/backend-app/main.py b/backend-app/main.py index 157097e..2b46c71 100644 --- a/backend-app/main.py +++ b/backend-app/main.py @@ -2,8 +2,7 @@ import uvicorn from fastapi import FastAPI, HTTPException from fastapi.responses import ORJSONResponse from core.settings import settings -from api.main_router import router as main_router -from api.another_router import router as another_router +from api import router from core.models import db_helper from contextlib import asynccontextmanager @@ -21,15 +20,6 @@ main_app = FastAPI( #api/hello -main_app.include_router( - router=main_router, - prefix="/api", - tags=["Основной роутер"] - ) -main_app.include_router( - router=another_router, - prefix="/another", - tags=["Побочный роутер"] - ) +main_app.include_router(router) if __name__ == "__main__": uvicorn.run(main_app, host=settings.run.host, port=settings.run.port) \ No newline at end of file diff --git a/backend-app/repo/crud.py b/backend-app/repo/crud/__init__.py similarity index 100% rename from backend-app/repo/crud.py rename to backend-app/repo/crud/__init__.py diff --git a/backend-app/repo/crud/login.py b/backend-app/repo/crud/login.py new file mode 100644 index 0000000..5aba2c6 --- /dev/null +++ b/backend-app/repo/crud/login.py @@ -0,0 +1,58 @@ +from typing import Sequence +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete, update +from ..schemas import LoginRead, LoginCreate, LoginUpdate +from ..models import LoginModel + +from utils.hashing import get_password_hash + +async def get_all_logins( + session: AsyncSession +) -> Sequence[LoginModel]: + stmt = select(LoginModel).order_by(LoginModel.id) + result = await session.scalars(stmt) + return result.all() + +async def get_login( + session: AsyncSession, + login_id: int +) -> LoginModel: + db_login = await session.get(LoginModel,login_id) + if db_login is None: + return None + return db_login + +async def create_login( + session:AsyncSession, + login_create: LoginCreate +) -> LoginModel: + login = LoginModel(**login_create.model_dump()) + login.password = get_password_hash(login.password) + session.add(login) + await session.commit() + return login + +async def delete_login( + session: AsyncSession, + login_id: int +) -> LoginModel: + db_login = await session.get(LoginModel,login_id) + if db_login is None: + return None + stmt = delete(LoginModel).filter(LoginModel.id == login_id) + await session.execute(stmt) + await session.commit() + return db_login.one_or_none() + +async def update_login( + session:AsyncSession, + login_id: int, + login_update: LoginUpdate +) -> LoginModel: + db_login = await session.get(LoginModel,login_id) + if db_login is None: + return None + for var, value in vars(login_update).items(): + setattr(db_login, var, value if var != "password" else get_password_hash(value)) if value else None + await session.commit() + return db_login diff --git a/backend-app/repo/crud/user.py b/backend-app/repo/crud/user.py new file mode 100644 index 0000000..60471d2 --- /dev/null +++ b/backend-app/repo/crud/user.py @@ -0,0 +1,58 @@ +from typing import Sequence +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete, update +from ..schemas import UserRead, UserCreate,UserUpdate +from ..models import UserModel + + + +async def get_all_users( + session: AsyncSession +) -> Sequence[UserModel]: + stmt = select(UserModel).order_by(UserModel.id) + result = await session.scalars(stmt) + return result.all() + +async def get_user( + session: AsyncSession, + user_id: int +) -> UserModel: + db_user = await session.get(UserModel, user_id) + if db_user is None: + return None + return db_user + +async def create_user( + session:AsyncSession, + user_create: UserCreate +) -> UserModel: + user = UserModel(**user_create.model_dump()) + session.add(user) + await session.commit() + return user + +async def delete_user( + session: AsyncSession, + user_id: int +) -> UserModel: + db_user = await session.get(UserModel, user_id) + if db_user is None: + return None + stmt = delete(UserModel).filter(UserModel.id == user_id) + await session.execute(stmt) + await session.commit() + return db_user.one_or_none() + +async def update_user( + session:AsyncSession, + user_id: int, + user_update: UserUpdate +) -> UserModel: + db_user = await session.get(UserModel, user_id) + if db_user is None: + return None + for var, value in vars(user_update).items(): + setattr(db_user, var, value) if value else None + await session.commit() + await session.refresh(db_user) + return db_user diff --git a/backend-app/repo/schemas.py b/backend-app/repo/schemas.py index 8d86299..5a8f36f 100644 --- a/backend-app/repo/schemas.py +++ b/backend-app/repo/schemas.py @@ -1,2 +1,33 @@ -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr +from typing import Optional, Sequence + +class LoginBase(BaseModel): + username:str + password:str + +class LoginUpdate(BaseModel): + username:Optional[str] = None + password:Optional[str] = None + +class LoginCreate(LoginBase): + pass + +class LoginRead(LoginBase): + id:int + +class UserUpdate(BaseModel): + firstname:Optional[str] = None + lastname:Optional[str] = None + age: Optional[int] = None + email: Optional[EmailStr] = None + +class UserBase(UserUpdate): + login_id: int + +class UserCreate(UserBase): + pass + + +class UserRead(UserBase): + id:int \ No newline at end of file diff --git a/backend-app/utils/hashing.py b/backend-app/utils/hashing.py new file mode 100644 index 0000000..eddb677 --- /dev/null +++ b/backend-app/utils/hashing.py @@ -0,0 +1,9 @@ +from passlib.context import CryptContext +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 26b3108..459b300 100644 --- a/poetry.lock +++ b/poetry.lock @@ -64,6 +64,46 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "bcrypt" +version = "4.2.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, + {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, + {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, + {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, + {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, + {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, + {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, + {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, + {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, + {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, + {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "black" version = "24.4.2" @@ -629,6 +669,23 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + [[package]] name = "pathspec" version = "0.12.1" @@ -1353,4 +1410,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "c1080a2638fb671a736b742ed0b4f44863e6cd4d3f36153e4f5f20fed11cf559" +content-hash = "ad4b72b47db69c4361a78a797531725b35c4810580a1f160bcf8f57872f61ffd" diff --git a/pyproject.toml b/pyproject.toml index 084e147..6aa19e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,5 @@ aioodbc = "^0.5.0" orjson = "^3.10.6" alembic = "^1.13.2" black = "^24.4.2" +passlib = "^1.7.4" +bcrypt = "^4.2.0"