diff --git a/api/app_builder/__init__.py b/api/app_entrypoint/__init__.py similarity index 100% rename from api/app_builder/__init__.py rename to api/app_entrypoint/__init__.py diff --git a/api/app_builder/dependencies.py b/api/app_entrypoint/dependencies.py similarity index 78% rename from api/app_builder/dependencies.py rename to api/app_entrypoint/dependencies.py index d5f8280..cf90398 100644 --- a/api/app_builder/dependencies.py +++ b/api/app_entrypoint/dependencies.py @@ -7,6 +7,8 @@ from api.application.protocols.jwt import JwtTokenProcessor from api.application.protocols.password_hasher import PasswordHasher from api.application.usecase.auth.auth_user import LoginUser from api.application.usecase.auth.create_user import CreateUser +from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail +from api.domain.company.repository import CompanyRepository from api.domain.user.repository import UserRepository from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.dependencies.adapters import ( @@ -26,8 +28,14 @@ from api.infrastructure.dependencies.protocols import ( get_password_hasher, get_user_login, ) -from api.infrastructure.dependencies.repositories import get_user_repository -from api.infrastructure.dependencies.usecases import provide_create_user +from api.infrastructure.dependencies.repositories import ( + get_company_repository, + get_user_repository, +) +from api.infrastructure.dependencies.usecases import ( + provide_create_user, + provide_get_companies_by_email, +) from api.infrastructure.persistence.db_setings import DBSettings from api.infrastructure.settings import Settings @@ -50,5 +58,7 @@ def init_dependencies(app: FastAPI) -> None: app.dependency_overrides[LoginUser] = get_user_login app.dependency_overrides[UserRepository] = get_user_repository + app.dependency_overrides[CompanyRepository] = get_company_repository app.dependency_overrides[CreateUser] = provide_create_user + app.dependency_overrides[GetCompaniesByOwnerEmail] = provide_get_companies_by_email diff --git a/api/app_builder/error_handlers.py b/api/app_entrypoint/error_handlers.py similarity index 100% rename from api/app_builder/error_handlers.py rename to api/app_entrypoint/error_handlers.py diff --git a/api/app_builder/main.py b/api/app_entrypoint/main.py similarity index 92% rename from api/app_builder/main.py rename to api/app_entrypoint/main.py index 8a1c7fb..bad1e6b 100644 --- a/api/app_builder/main.py +++ b/api/app_entrypoint/main.py @@ -4,8 +4,8 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from sqlalchemy.ext.asyncio import AsyncEngine -from api.app_builder.dependencies import init_dependencies -from api.app_builder.error_handlers import init_exc_handlers +from api.app_entrypoint.dependencies import init_dependencies +from api.app_entrypoint.error_handlers import init_exc_handlers from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.dependencies.adapters import create_engine from api.infrastructure.dependencies.configs import ( diff --git a/api/app_builder/routers.py b/api/app_entrypoint/routers.py similarity index 100% rename from api/app_builder/routers.py rename to api/app_entrypoint/routers.py diff --git a/api/application/protocols/jwt.py b/api/application/protocols/jwt.py index a5ea97d..d536959 100644 --- a/api/application/protocols/jwt.py +++ b/api/application/protocols/jwt.py @@ -1,13 +1,13 @@ from typing import Protocol -from api.domain.user.model import UserId +from api.domain.user.model import UserEmail, UserId class JwtTokenProcessor(Protocol): - def generate_token(self, user_id: UserId) -> str: + def generate_token(self, user_id: UserId, user_email: UserEmail) -> str: raise NotImplementedError - def validate_token(self, token: str) -> UserId | None: + def validate_token(self, token: str) -> tuple[UserId, UserEmail] | None: raise NotImplementedError def refresh_token(self, token: str) -> str: diff --git a/api/domain/company/model.py b/api/domain/company/model.py index 08dca89..32577c0 100644 --- a/api/domain/company/model.py +++ b/api/domain/company/model.py @@ -4,7 +4,6 @@ from uuid import UUID, uuid4 from api.domain import DomainValidationError from api.domain.entity import DomainEntity -from api.domain.user.model import User from api.domain.value_obj import DomainValueObject @@ -41,13 +40,11 @@ class CompanyId(DomainValueObject): class Company(DomainEntity[CompanyId]): name: CompanyName email: CompanyEmail - owner: User @staticmethod - def create(name: str, email: str, owner: User) -> "Company": + def create(name: str, email: str) -> "Company": return Company( id=CompanyId(uuid4()), name=CompanyName(name), email=CompanyEmail(email), - owner=owner, ) diff --git a/api/domain/company/repository.py b/api/domain/company/repository.py index ffd0f54..dea743f 100644 --- a/api/domain/company/repository.py +++ b/api/domain/company/repository.py @@ -1,15 +1,14 @@ from typing import Protocol from api.domain.company.model import Company -from api.domain.user.model import User class CompanyRepository(Protocol): async def get_companies_by_owner_email(self, filter: dict) -> list[Company]: raise NotImplementedError - async def create_company(self, company: Company) -> None: - raise NotImplementedError - - async def get_workes_list(self) -> list[User]: - raise NotImplementedError + # async def create_company(self, company: Company) -> None: + # raise NotImplementedError + # + # async def get_workes_list(self) -> list[User]: + # raise NotImplementedError diff --git a/api/infrastructure/auth/jwt_processor.py b/api/infrastructure/auth/jwt_processor.py index 67b33ae..c67f64b 100644 --- a/api/infrastructure/auth/jwt_processor.py +++ b/api/infrastructure/auth/jwt_processor.py @@ -7,7 +7,7 @@ from jose.jwt import decode, encode from api.application.protocols.date_time import DateTimeProvider from api.application.protocols.jwt import JwtTokenProcessor from api.domain.user.error import UserInvalidCredentialsError -from api.domain.user.model import UserId +from api.domain.user.model import UserEmail, UserId from api.infrastructure.auth.jwt_settings import JwtSettings @@ -16,7 +16,7 @@ class JoseJwtTokenProcessor(JwtTokenProcessor): self.jwt_options = jwt_options self.date_time_provider = date_time_provider - def generate_token(self, user_id: UserId) -> str: + def generate_token(self, user_id: UserId, user_email: UserEmail) -> str: issued_at = self.date_time_provider.get_current_time() expiration_time = issued_at + timedelta(minutes=self.jwt_options.expires_in) @@ -24,19 +24,20 @@ class JoseJwtTokenProcessor(JwtTokenProcessor): "iat": issued_at, "exp": expiration_time, "sub": str(user_id.value), + "email": user_email.value, } return encode(claims, self.jwt_options.secret, self.jwt_options.algorithm) - def validate_token(self, token: str) -> UserId | None: + def validate_token(self, token: str) -> tuple[UserId, UserEmail] | None: try: payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm]) - return UserId(UUID(payload["sub"])) + return UserId(UUID(payload["sub"])), UserEmail(payload["email"]) except (JWTError, ValueError, KeyError): return None def refresh_token(self, token: str) -> str: - user = self.validate_token(token) - if user is None: + token_data = self.validate_token(token) + if token_data is None: raise UserInvalidCredentialsError("invalid token") - return self.generate_token(user) + return self.generate_token(token_data[0], token_data[1]) diff --git a/api/infrastructure/dependencies/repositories.py b/api/infrastructure/dependencies/repositories.py index 6a97c66..eba1f43 100644 --- a/api/infrastructure/dependencies/repositories.py +++ b/api/infrastructure/dependencies/repositories.py @@ -3,7 +3,11 @@ from typing import Annotated from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession +from api.domain.company.repository import CompanyRepository from api.domain.user import UserRepository +from api.infrastructure.persistence.repositories.company_repository import ( + SqlAlchemyCompanyRepository, +) from api.infrastructure.persistence.repositories.user_repository import ( SqlAlchemyUserRepository, ) @@ -15,3 +19,9 @@ def get_user_repository( session: Annotated[AsyncSession, Depends(Stub(AsyncSession))], ) -> UserRepository: return SqlAlchemyUserRepository(session) + + +def get_company_repository( + session: Annotated[AsyncSession, Depends(Stub(AsyncSession))], +) -> CompanyRepository: + return SqlAlchemyCompanyRepository(session) diff --git a/api/infrastructure/dependencies/usecases.py b/api/infrastructure/dependencies/usecases.py index 3c33cf7..80b0170 100644 --- a/api/infrastructure/dependencies/usecases.py +++ b/api/infrastructure/dependencies/usecases.py @@ -5,6 +5,8 @@ from fastapi import Depends from api.application.abstractions.uow import UnitOfWork from api.application.protocols.password_hasher import PasswordHasher from api.application.usecase.auth.create_user import CreateUser +from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail +from api.domain.company.repository import CompanyRepository from api.domain.user.repository import UserRepository from api.infrastructure.dependencies.stub import Stub @@ -15,3 +17,9 @@ def provide_create_user( password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))], ) -> CreateUser: return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher) + + +def provide_get_companies_by_email( + company_repository: Annotated[CompanyRepository, Depends(Stub(CompanyRepository))], +) -> GetCompaniesByOwnerEmail: + return GetCompaniesByOwnerEmail(company_repository=company_repository) diff --git a/api/infrastructure/persistence/models/__init__.py b/api/infrastructure/persistence/models/__init__.py index 5c7c96a..02d80c9 100644 --- a/api/infrastructure/persistence/models/__init__.py +++ b/api/infrastructure/persistence/models/__init__.py @@ -1,7 +1,9 @@ from .base import Base +from .company import CompanyModel from .user import UserModel __all__ = ( "Base", "UserModel", + "CompanyModel", ) diff --git a/api/infrastructure/persistence/models/company.py b/api/infrastructure/persistence/models/company.py new file mode 100644 index 0000000..0890847 --- /dev/null +++ b/api/infrastructure/persistence/models/company.py @@ -0,0 +1,17 @@ +import uuid + +from sqlalchemy import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from api.infrastructure.persistence.models.base import Base + + +class CompanyModel(Base): + __tablename__ = "companies" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + primary_key=True, + ) + name: Mapped[str] + email: Mapped[str] = mapped_column(unique=True) diff --git a/api/infrastructure/persistence/repositories/company_repository.py b/api/infrastructure/persistence/repositories/company_repository.py index 039d555..71e99ef 100644 --- a/api/infrastructure/persistence/repositories/company_repository.py +++ b/api/infrastructure/persistence/repositories/company_repository.py @@ -1,45 +1,45 @@ -# from sqlalchemy import text -# from sqlalchemy.ext.asyncio import AsyncSession -# -# from api.domain.company import CompanyRepository, company -# from api.domain.user.model import UserEmail, UserFirstName, UserId -# -# -# class SqlAlchemyUserRepository(UserRepository): -# def __init__(self, session: AsyncSession) -> None: -# self.session = session -# -# async def create_user(self, user: User) -> None: -# stmt = text( -# """INSERT INTO users (id, name, email, hashed_password) -# VALUES(:id, :name, :email, :hashed_password) -# """ -# ) -# await self.session.execute( -# stmt, -# { -# "id": str(user.id.value), -# "name": user.name.value, -# "email": user.email.value, -# "hashed_password": user.hashed_password, -# }, -# ) -# -# async def get_user(self, filter: dict) -> User | None: -# stmt = text("""SELECT * FROM users WHERE email = :val""") -# result = await self.session.execute(stmt, {"val": filter["email"]}) -# -# result = result.mappings().one_or_none() -# -# if result is None: -# return None -# -# return User( -# id=UserId(result.id), -# name=UserFirstName(result.name), -# email=UserEmail(result.email), -# hashed_password=result.hashed_password, -# ) -# +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession + +from api.domain.company.model import Company, CompanyEmail, CompanyId, CompanyName +from api.domain.company.repository import CompanyRepository + + +class SqlAlchemyCompanyRepository(CompanyRepository): + def __init__(self, session: AsyncSession) -> None: + self.session = session + + # async def create_user(self, user: User) -> None: + # stmt = text( + # """INSERT INTO users (id, name, email, hashed_password) + # VALUES(:id, :name, :email, :hashed_password) + # """ + # ) + # await self.session.execute( + # stmt, + # { + # "id": str(user.id.value), + # "name": user.name.value, + # "email": user.email.value, + # "hashed_password": user.hashed_password, + # }, + # ) + # + async def get_companies_by_owner_email(self, filter: dict) -> list[Company]: + stmt = text("""SELECT * FROM companies WHERE email = :val""") + result = await self.session.execute(stmt, {"val": filter["email"]}) + + result = result.mappings().all() + + return [ + Company( + id=CompanyId(c.id), + name=CompanyName(c.name), + email=CompanyEmail(c.email), + ) + for c in result + ] + + # async def get_users(self) -> list[User]: # return [] diff --git a/api/presentation/routers/auth.py b/api/presentation/routers/auth.py index 85f2d28..a9cb461 100644 --- a/api/presentation/routers/auth.py +++ b/api/presentation/routers/auth.py @@ -8,7 +8,7 @@ from api.application.contracts.auth.auth_response import AuthenticationResponse from api.application.protocols.jwt import JwtTokenProcessor from api.application.usecase.auth.auth_user import LoginUser from api.application.usecase.auth.create_user import CreateUser -from api.domain.user.model import UserId +from api.domain.user.model import UserEmail, UserId from api.infrastructure.dependencies.stub import Stub auth_router = APIRouter(prefix="/auth", tags=["Auth"]) @@ -35,7 +35,7 @@ async def login( password=login_request.password, ) ) - token = token_processor.generate_token(UserId(user.id)) + token = token_processor.generate_token(UserId(user.id), UserEmail(user.email)) response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True) return user diff --git a/api/presentation/routers/company.py b/api/presentation/routers/company.py index e2a304b..ed5498a 100644 --- a/api/presentation/routers/company.py +++ b/api/presentation/routers/company.py @@ -1,6 +1,13 @@ +from typing import Annotated + from fastapi import APIRouter, Depends, Request +from api.application.contracts.company.company_request import CompanyByOwnerEmail from api.application.contracts.company.company_response import CompanyBaseResponse +from api.application.protocols.jwt import JwtTokenProcessor +from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail +from api.domain.user.error import UserValidationError +from api.infrastructure.dependencies.stub import Stub from api.presentation.auth.fasapi_auth import auth_required company_router = APIRouter(prefix="/company", tags=["Company"]) @@ -11,8 +18,19 @@ company_router = APIRouter(prefix="/company", tags=["Company"]) response_model=None, dependencies=[Depends(auth_required)], ) -async def get_company(request: Request) -> CompanyBaseResponse: - return CompanyBaseResponse( - name="some", - email="some", - ) +async def get_companies( + request: Request, + token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))], + usecase: Annotated[GetCompaniesByOwnerEmail, Depends(Stub(GetCompaniesByOwnerEmail))], +) -> list[CompanyBaseResponse]: + token_data = token_processor.validate_token(request.scope["auth"]) + if not token_data: + raise UserValidationError("Login required") + companies = await usecase.execute(request=CompanyByOwnerEmail(email=token_data[1].value)) + return [ + CompanyBaseResponse( + name=c.name, + email=c.email, + ) + for c in companies + ] diff --git a/config/api_config.yml b/config/api_config.yml index 37c39ea..a5116a7 100644 --- a/config/api_config.yml +++ b/config/api_config.yml @@ -1,5 +1,5 @@ db: - host: "localhost" + host: "db" port: 5432 database: "serviceman_db" user: "demo_user" diff --git a/docker-compose.yml b/docker-compose.yml index c571f96..be9cdee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: - ./api:/usr/src/service_man/api # - ./alembic.ini:/usr/src/service_man/alembic.ini - command: /bin/bash -c 'cd /usr/src/service_man && poetry run uvicorn api.app_builder.main:app_factory --host 0.0.0.0 --reload --factory' + command: /bin/bash -c 'poetry run uvicorn api.app_entrypoint.main:app_factory --host 0.0.0.0 --reload --factory' # bot: # container_name: bot diff --git a/manage.py b/manage.py index ecc58d3..add5137 100644 --- a/manage.py +++ b/manage.py @@ -2,7 +2,7 @@ if __name__ == "__main__": import uvicorn uvicorn.run( - app="api.app_builder.main:app_factory", + app="api.app_entrypoint.main:app_factory", host="0.0.0.0", port=8000, reload=True, diff --git a/poetry.lock b/poetry.lock index 907aa2e..0fa3af7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1196,7 +1196,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1565,4 +1564,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "586dbb5fd80f9999d39a1cb960a06bc8f15e3b103f1c273e4d954fca0b1ffc75" +content-hash = "e718faa94188a6831502fbe2b89da05cc97f959502b671761995673aa0f018b8" diff --git a/pyproject.toml b/pyproject.toml index 1cb3e1b..91abc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,6 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" -python-jose = "^3.3.0" -python-multipart = "^0.0.9" - - [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.2" @@ -27,6 +23,8 @@ uvicorn = "^0.27.1" pyyaml = "^6.0.1" alembic = "^1.13.1" passlib = "^1.7.4" +python-jose = "^3.3.0" +python-multipart = "^0.0.9" [tool.poetry.group.bot.dependencies] aiogram = "^3.4.1"