add getting company

main
Сергей Ванюшкин 2024-04-10 00:33:31 +03:00
parent 361378929f
commit 0e2ecd3449
21 changed files with 142 additions and 83 deletions

View File

@ -7,6 +7,8 @@ from api.application.protocols.jwt import JwtTokenProcessor
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.auth_user import LoginUser from api.application.usecase.auth.auth_user import LoginUser
from api.application.usecase.auth.create_user import CreateUser 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.domain.user.repository import UserRepository
from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import ( from api.infrastructure.dependencies.adapters import (
@ -26,8 +28,14 @@ from api.infrastructure.dependencies.protocols import (
get_password_hasher, get_password_hasher,
get_user_login, get_user_login,
) )
from api.infrastructure.dependencies.repositories import get_user_repository from api.infrastructure.dependencies.repositories import (
from api.infrastructure.dependencies.usecases import provide_create_user 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.persistence.db_setings import DBSettings
from api.infrastructure.settings import Settings 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[LoginUser] = get_user_login
app.dependency_overrides[UserRepository] = get_user_repository app.dependency_overrides[UserRepository] = get_user_repository
app.dependency_overrides[CompanyRepository] = get_company_repository
app.dependency_overrides[CreateUser] = provide_create_user app.dependency_overrides[CreateUser] = provide_create_user
app.dependency_overrides[GetCompaniesByOwnerEmail] = provide_get_companies_by_email

View File

@ -4,8 +4,8 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import AsyncEngine
from api.app_builder.dependencies import init_dependencies from api.app_entrypoint.dependencies import init_dependencies
from api.app_builder.error_handlers import init_exc_handlers from api.app_entrypoint.error_handlers import init_exc_handlers
from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import create_engine from api.infrastructure.dependencies.adapters import create_engine
from api.infrastructure.dependencies.configs import ( from api.infrastructure.dependencies.configs import (

View File

@ -1,13 +1,13 @@
from typing import Protocol from typing import Protocol
from api.domain.user.model import UserId from api.domain.user.model import UserEmail, UserId
class JwtTokenProcessor(Protocol): class JwtTokenProcessor(Protocol):
def generate_token(self, user_id: UserId) -> str: def generate_token(self, user_id: UserId, user_email: UserEmail) -> str:
raise NotImplementedError raise NotImplementedError
def validate_token(self, token: str) -> UserId | None: def validate_token(self, token: str) -> tuple[UserId, UserEmail] | None:
raise NotImplementedError raise NotImplementedError
def refresh_token(self, token: str) -> str: def refresh_token(self, token: str) -> str:

View File

@ -4,7 +4,6 @@ from uuid import UUID, uuid4
from api.domain import DomainValidationError from api.domain import DomainValidationError
from api.domain.entity import DomainEntity from api.domain.entity import DomainEntity
from api.domain.user.model import User
from api.domain.value_obj import DomainValueObject from api.domain.value_obj import DomainValueObject
@ -41,13 +40,11 @@ class CompanyId(DomainValueObject):
class Company(DomainEntity[CompanyId]): class Company(DomainEntity[CompanyId]):
name: CompanyName name: CompanyName
email: CompanyEmail email: CompanyEmail
owner: User
@staticmethod @staticmethod
def create(name: str, email: str, owner: User) -> "Company": def create(name: str, email: str) -> "Company":
return Company( return Company(
id=CompanyId(uuid4()), id=CompanyId(uuid4()),
name=CompanyName(name), name=CompanyName(name),
email=CompanyEmail(email), email=CompanyEmail(email),
owner=owner,
) )

View File

@ -1,15 +1,14 @@
from typing import Protocol from typing import Protocol
from api.domain.company.model import Company from api.domain.company.model import Company
from api.domain.user.model import User
class CompanyRepository(Protocol): class CompanyRepository(Protocol):
async def get_companies_by_owner_email(self, filter: dict) -> list[Company]: async def get_companies_by_owner_email(self, filter: dict) -> list[Company]:
raise NotImplementedError raise NotImplementedError
async def create_company(self, company: Company) -> None: # async def create_company(self, company: Company) -> None:
raise NotImplementedError # raise NotImplementedError
#
async def get_workes_list(self) -> list[User]: # async def get_workes_list(self) -> list[User]:
raise NotImplementedError # raise NotImplementedError

View File

@ -7,7 +7,7 @@ from jose.jwt import decode, encode
from api.application.protocols.date_time import DateTimeProvider from api.application.protocols.date_time import DateTimeProvider
from api.application.protocols.jwt import JwtTokenProcessor from api.application.protocols.jwt import JwtTokenProcessor
from api.domain.user.error import UserInvalidCredentialsError 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 from api.infrastructure.auth.jwt_settings import JwtSettings
@ -16,7 +16,7 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
self.jwt_options = jwt_options self.jwt_options = jwt_options
self.date_time_provider = date_time_provider 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() issued_at = self.date_time_provider.get_current_time()
expiration_time = issued_at + timedelta(minutes=self.jwt_options.expires_in) expiration_time = issued_at + timedelta(minutes=self.jwt_options.expires_in)
@ -24,19 +24,20 @@ class JoseJwtTokenProcessor(JwtTokenProcessor):
"iat": issued_at, "iat": issued_at,
"exp": expiration_time, "exp": expiration_time,
"sub": str(user_id.value), "sub": str(user_id.value),
"email": user_email.value,
} }
return encode(claims, self.jwt_options.secret, self.jwt_options.algorithm) 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: try:
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm]) 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): except (JWTError, ValueError, KeyError):
return None return None
def refresh_token(self, token: str) -> str: def refresh_token(self, token: str) -> str:
user = self.validate_token(token) token_data = self.validate_token(token)
if user is None: if token_data is None:
raise UserInvalidCredentialsError("invalid token") raise UserInvalidCredentialsError("invalid token")
return self.generate_token(user) return self.generate_token(token_data[0], token_data[1])

View File

@ -3,7 +3,11 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.company.repository import CompanyRepository
from api.domain.user import UserRepository from api.domain.user import UserRepository
from api.infrastructure.persistence.repositories.company_repository import (
SqlAlchemyCompanyRepository,
)
from api.infrastructure.persistence.repositories.user_repository import ( from api.infrastructure.persistence.repositories.user_repository import (
SqlAlchemyUserRepository, SqlAlchemyUserRepository,
) )
@ -15,3 +19,9 @@ def get_user_repository(
session: Annotated[AsyncSession, Depends(Stub(AsyncSession))], session: Annotated[AsyncSession, Depends(Stub(AsyncSession))],
) -> UserRepository: ) -> UserRepository:
return SqlAlchemyUserRepository(session) return SqlAlchemyUserRepository(session)
def get_company_repository(
session: Annotated[AsyncSession, Depends(Stub(AsyncSession))],
) -> CompanyRepository:
return SqlAlchemyCompanyRepository(session)

View File

@ -5,6 +5,8 @@ from fastapi import Depends
from api.application.abstractions.uow import UnitOfWork from api.application.abstractions.uow import UnitOfWork
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.create_user import CreateUser 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.domain.user.repository import UserRepository
from api.infrastructure.dependencies.stub import Stub from api.infrastructure.dependencies.stub import Stub
@ -15,3 +17,9 @@ def provide_create_user(
password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))], password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))],
) -> CreateUser: ) -> CreateUser:
return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher) 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)

View File

@ -1,7 +1,9 @@
from .base import Base from .base import Base
from .company import CompanyModel
from .user import UserModel from .user import UserModel
__all__ = ( __all__ = (
"Base", "Base",
"UserModel", "UserModel",
"CompanyModel",
) )

View File

@ -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)

View File

@ -1,45 +1,45 @@
# from sqlalchemy import text from sqlalchemy import text
# from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
#
# from api.domain.company import CompanyRepository, company from api.domain.company.model import Company, CompanyEmail, CompanyId, CompanyName
# from api.domain.user.model import UserEmail, UserFirstName, UserId from api.domain.company.repository import CompanyRepository
#
#
# class SqlAlchemyUserRepository(UserRepository): class SqlAlchemyCompanyRepository(CompanyRepository):
# def __init__(self, session: AsyncSession) -> None: def __init__(self, session: AsyncSession) -> None:
# self.session = session self.session = session
#
# async def create_user(self, user: User) -> None: # async def create_user(self, user: User) -> None:
# stmt = text( # stmt = text(
# """INSERT INTO users (id, name, email, hashed_password) # """INSERT INTO users (id, name, email, hashed_password)
# VALUES(:id, :name, :email, :hashed_password) # VALUES(:id, :name, :email, :hashed_password)
# """ # """
# ) # )
# await self.session.execute( # await self.session.execute(
# stmt, # stmt,
# { # {
# "id": str(user.id.value), # "id": str(user.id.value),
# "name": user.name.value, # "name": user.name.value,
# "email": user.email.value, # "email": user.email.value,
# "hashed_password": user.hashed_password, # "hashed_password": user.hashed_password,
# }, # },
# ) # )
# #
# async def get_user(self, filter: dict) -> User | None: async def get_companies_by_owner_email(self, filter: dict) -> list[Company]:
# stmt = text("""SELECT * FROM users WHERE email = :val""") stmt = text("""SELECT * FROM companies WHERE email = :val""")
# result = await self.session.execute(stmt, {"val": filter["email"]}) result = await self.session.execute(stmt, {"val": filter["email"]})
#
# result = result.mappings().one_or_none() result = result.mappings().all()
#
# if result is None: return [
# return None Company(
# id=CompanyId(c.id),
# return User( name=CompanyName(c.name),
# id=UserId(result.id), email=CompanyEmail(c.email),
# name=UserFirstName(result.name), )
# email=UserEmail(result.email), for c in result
# hashed_password=result.hashed_password, ]
# )
#
# async def get_users(self) -> list[User]: # async def get_users(self) -> list[User]:
# return [] # return []

View File

@ -8,7 +8,7 @@ from api.application.contracts.auth.auth_response import AuthenticationResponse
from api.application.protocols.jwt import JwtTokenProcessor from api.application.protocols.jwt import JwtTokenProcessor
from api.application.usecase.auth.auth_user import LoginUser from api.application.usecase.auth.auth_user import LoginUser
from api.application.usecase.auth.create_user import CreateUser 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 from api.infrastructure.dependencies.stub import Stub
auth_router = APIRouter(prefix="/auth", tags=["Auth"]) auth_router = APIRouter(prefix="/auth", tags=["Auth"])
@ -35,7 +35,7 @@ async def login(
password=login_request.password, 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) response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
return user return user

View File

@ -1,6 +1,13 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Request 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.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 from api.presentation.auth.fasapi_auth import auth_required
company_router = APIRouter(prefix="/company", tags=["Company"]) company_router = APIRouter(prefix="/company", tags=["Company"])
@ -11,8 +18,19 @@ company_router = APIRouter(prefix="/company", tags=["Company"])
response_model=None, response_model=None,
dependencies=[Depends(auth_required)], dependencies=[Depends(auth_required)],
) )
async def get_company(request: Request) -> CompanyBaseResponse: async def get_companies(
return CompanyBaseResponse( request: Request,
name="some", token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
email="some", 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
]

View File

@ -1,5 +1,5 @@
db: db:
host: "localhost" host: "db"
port: 5432 port: 5432
database: "serviceman_db" database: "serviceman_db"
user: "demo_user" user: "demo_user"

View File

@ -44,7 +44,7 @@ services:
- ./api:/usr/src/service_man/api - ./api:/usr/src/service_man/api
# - ./alembic.ini:/usr/src/service_man/alembic.ini # - ./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: # bot:
# container_name: bot # container_name: bot

View File

@ -2,7 +2,7 @@ if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run( uvicorn.run(
app="api.app_builder.main:app_factory", app="api.app_entrypoint.main:app_factory",
host="0.0.0.0", host="0.0.0.0",
port=8000, port=8000,
reload=True, reload=True,

3
poetry.lock generated
View File

@ -1196,7 +1196,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {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_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-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-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-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1565,4 +1564,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "586dbb5fd80f9999d39a1cb960a06bc8f15e3b103f1c273e4d954fca0b1ffc75" content-hash = "e718faa94188a6831502fbe2b89da05cc97f959502b671761995673aa0f018b8"

View File

@ -7,10 +7,6 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
python-jose = "^3.3.0"
python-multipart = "^0.0.9"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pre-commit = "^3.6.2" pre-commit = "^3.6.2"
@ -27,6 +23,8 @@ uvicorn = "^0.27.1"
pyyaml = "^6.0.1" pyyaml = "^6.0.1"
alembic = "^1.13.1" alembic = "^1.13.1"
passlib = "^1.7.4" passlib = "^1.7.4"
python-jose = "^3.3.0"
python-multipart = "^0.0.9"
[tool.poetry.group.bot.dependencies] [tool.poetry.group.bot.dependencies]
aiogram = "^3.4.1" aiogram = "^3.4.1"