From 3d26a7f2128a7b60a9ffa5e7a629105d663596a5 Mon Sep 17 00:00:00 2001 From: Archibald Date: Sat, 19 Apr 2025 13:01:45 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=20=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API/app/application/profiles_repository.py | 14 ++++ API/app/application/roles_repository.py | 16 ++++ API/app/application/teams_repository.py | 16 ++++ API/app/application/users_repository.py | 31 ++++++++ API/app/domain/entities/base_profile.py | 19 +++++ API/app/domain/entities/base_user.py | 9 +++ API/app/domain/entities/profile.py | 7 ++ API/app/domain/entities/register.py | 6 ++ API/app/domain/entities/user.py | 12 +++ API/app/domain/models/profile_photos.py | 2 +- API/app/domain/models/profiles.py | 2 +- API/app/domain/models/users.py | 7 ++ API/app/infrastructure/users_service.py | 93 ++++++++++++++++++++++ API/app/main.py | 21 +++++ 14 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 API/app/application/profiles_repository.py create mode 100644 API/app/application/roles_repository.py create mode 100644 API/app/application/teams_repository.py create mode 100644 API/app/application/users_repository.py create mode 100644 API/app/domain/entities/base_profile.py create mode 100644 API/app/domain/entities/base_user.py create mode 100644 API/app/domain/entities/profile.py create mode 100644 API/app/domain/entities/register.py create mode 100644 API/app/domain/entities/user.py create mode 100644 API/app/infrastructure/users_service.py diff --git a/API/app/application/profiles_repository.py b/API/app/application/profiles_repository.py new file mode 100644 index 0000000..49bb390 --- /dev/null +++ b/API/app/application/profiles_repository.py @@ -0,0 +1,14 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import Profile + + +class ProfilesRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, profile: Profile) -> Profile: + self.db.add(profile) + await self.db.commit() + await self.db.refresh(profile) + return profile \ No newline at end of file diff --git a/API/app/application/roles_repository.py b/API/app/application/roles_repository.py new file mode 100644 index 0000000..718047f --- /dev/null +++ b/API/app/application/roles_repository.py @@ -0,0 +1,16 @@ +from select import select +from typing import Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import Role + + +class RolesRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def get_by_id(self, role_id: int) -> Optional[Role]: + stmt = select(Role).filter_by(id=role_id) + result = await self.db.execute(stmt) + return result.scalars().first() diff --git a/API/app/application/teams_repository.py b/API/app/application/teams_repository.py new file mode 100644 index 0000000..4d1b14b --- /dev/null +++ b/API/app/application/teams_repository.py @@ -0,0 +1,16 @@ +from select import select +from typing import Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import Team + + +class TeamsRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def get_by_id(self, team_id: int) -> Optional[Team]: + stmt = select(Team).filter_by(id=team_id) + result = await self.db.execute(stmt) + return result.scalars().first() diff --git a/API/app/application/users_repository.py b/API/app/application/users_repository.py new file mode 100644 index 0000000..1723055 --- /dev/null +++ b/API/app/application/users_repository.py @@ -0,0 +1,31 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload +from typing_extensions import Optional + +from app.domain.models import User + + +class UsersRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def get_by_id(self, user_id: int) -> Optional[User]: + stmt = select(User).filter_by(id=user_id) + result = await self.db.execute(stmt) + return result.scalars().first() + + async def get_by_login(self, login: str) -> Optional[User]: + stmt = ( + select(User) + .filter_by(login=login) + .options(joinedload(User.role)) + ) + result = await self.db.execute(stmt) + return result.scalars().first() + + async def create(self, user: User) -> User: + self.db.add(user) + await self.db.commit() + await self.db.refresh(user) + return user \ No newline at end of file diff --git a/API/app/domain/entities/base_profile.py b/API/app/domain/entities/base_profile.py new file mode 100644 index 0000000..8a4953a --- /dev/null +++ b/API/app/domain/entities/base_profile.py @@ -0,0 +1,19 @@ +import datetime +from typing import Optional + +from pydantic import BaseModel + + +class BaseProfileEntity(BaseModel): + first_name: str + last_name: str + patronymic: Optional[str] = None + birthday: datetime.date + email: Optional[str] = None + phone: Optional[str] = None + + role_id: int + team_id: int + + class Config: + abstract = True diff --git a/API/app/domain/entities/base_user.py b/API/app/domain/entities/base_user.py new file mode 100644 index 0000000..7a2875e --- /dev/null +++ b/API/app/domain/entities/base_user.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class BaseUserEntity(BaseModel): + login: str + password: str + + class Config: + abstract = True diff --git a/API/app/domain/entities/profile.py b/API/app/domain/entities/profile.py new file mode 100644 index 0000000..c57e728 --- /dev/null +++ b/API/app/domain/entities/profile.py @@ -0,0 +1,7 @@ +from typing import Optional + +from app.domain.entities.base_profile import BaseProfileEntity + + +class ProfileEntity(BaseProfileEntity): + id: Optional[int] = None \ No newline at end of file diff --git a/API/app/domain/entities/register.py b/API/app/domain/entities/register.py new file mode 100644 index 0000000..92f46ed --- /dev/null +++ b/API/app/domain/entities/register.py @@ -0,0 +1,6 @@ +from app.domain.entities.base_profile import BaseProfileEntity +from app.domain.entities.base_user import BaseUserEntity + + +class RegisterEntity(BaseUserEntity, BaseProfileEntity): + pass diff --git a/API/app/domain/entities/user.py b/API/app/domain/entities/user.py new file mode 100644 index 0000000..022c0eb --- /dev/null +++ b/API/app/domain/entities/user.py @@ -0,0 +1,12 @@ +from typing import Optional + +from app.domain.entities.base_user import BaseUserEntity +from app.domain.entities.profile import ProfileEntity + + +class UserEntity(BaseUserEntity): + id: Optional[int] = None + + profile_id: int + + profile: Optional[ProfileEntity] = None diff --git a/API/app/domain/models/profile_photos.py b/API/app/domain/models/profile_photos.py index 1b20294..da10867 100644 --- a/API/app/domain/models/profile_photos.py +++ b/API/app/domain/models/profile_photos.py @@ -11,4 +11,4 @@ class ProfilePhoto(AdvancedBaseModel): profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False) - profile = relationship('Profile', back_populates='photos') \ No newline at end of file + profile = relationship('Profile', back_populates='profile_photos') \ No newline at end of file diff --git a/API/app/domain/models/profiles.py b/API/app/domain/models/profiles.py index 9c3b80b..cc76e79 100644 --- a/API/app/domain/models/profiles.py +++ b/API/app/domain/models/profiles.py @@ -21,5 +21,5 @@ class Profile(AdvancedBaseModel): team = relationship('Team', back_populates='profiles') user = relationship('User', back_populates='profile') - profile_photo = relationship('ProfilePhoto', back_populates='profile') + profile_photos = relationship('ProfilePhoto', back_populates='profile') projects = relationship('ProjectMember', back_populates='profile') diff --git a/API/app/domain/models/users.py b/API/app/domain/models/users.py index ec5a0b4..5dc4668 100644 --- a/API/app/domain/models/users.py +++ b/API/app/domain/models/users.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, VARCHAR, ForeignKey, Integer from sqlalchemy.orm import relationship +from werkzeug.security import check_password_hash, generate_password_hash from app.domain.models.base import AdvancedBaseModel @@ -13,3 +14,9 @@ class User(AdvancedBaseModel): profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False) profile = relationship('Profile', back_populates='user') + + def check_password(self, password): + return check_password_hash(self.password, password) + + def set_password(self, password): + self.password = generate_password_hash(password) diff --git a/API/app/infrastructure/users_service.py b/API/app/infrastructure/users_service.py new file mode 100644 index 0000000..6726552 --- /dev/null +++ b/API/app/infrastructure/users_service.py @@ -0,0 +1,93 @@ +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.application.profiles_repository import ProfilesRepository +from app.application.roles_repository import RolesRepository +from app.application.teams_repository import TeamsRepository +from app.application.users_repository import UsersRepository +from app.domain.entities.register import RegisterEntity +from app.domain.entities.user import UserEntity +from app.domain.models import User, Profile + + +class UsersService: + def __init__(self, db: AsyncSession): + self.users_repository = UsersRepository(db) + self.teams_repository = TeamsRepository(db) + self.roles_repository = RolesRepository(db) + self.profiles_repository = ProfilesRepository(db) + + async def register_user(self, register_entity: RegisterEntity) -> Optional[UserEntity]: + team = await self.teams_repository.get_by_id(register_entity.team_id) + if team is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The team with this ID was not found", + ) + + role = await self.roles_repository.get_by_id(register_entity.role_id) + if role is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The role with this ID was not found", + ) + + user_model, profile_model = self.register_entity_to_models(register_entity) + + profile_model = await self.profiles_repository.create(profile_model) + user_model.profile_id = profile_model.id + user_model = await self.users_repository.create(user_model) + + return UserEntity + + @staticmethod + def is_strong_password(password): + if len(password) < 8: + return False + + if not any(char.isupper() for char in password): + return False + + if not any(char.islower() for char in password): + return False + + if not any(char.isdigit() for char in password): + return False + + if not any(char in "!@#$%^&*()_+" for char in password): + return False + + if not any(char.isalpha() for char in password): + return False + + return True + + @staticmethod + def register_entity_to_models(register_entity: RegisterEntity) -> tuple[User, Profile]: + user = User( + login=register_entity.login, + ) + + user.set_password(register_entity.password) + + pofile = Profile( + first_name=register_entity.first_name, + last_name=register_entity.last_name, + patronymic=register_entity.patronymic, + birthday=register_entity.birthday, + email=register_entity.email, + phone=register_entity.phone, + role_id=register_entity.role_id, + team_id=register_entity.team_id + ) + + return user, pofile + + @staticmethod + def user_model_to_entity(user_model: User) -> UserEntity: + return UserEntity( + id=user_model.id, + login=user_model.login, + ) diff --git a/API/app/main.py b/API/app/main.py index e69de29..a416972 100644 --- a/API/app/main.py +++ b/API/app/main.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +def start_app(): + api_app = FastAPI() + + api_app.add_middleware( + CORSMiddleware, + allow_origins=["http//localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + return api_app + +app = start_app() + +@app.get("/") +async def root(): + return {"message": "Hello API"} \ No newline at end of file