diff --git a/API/app/application/teams_repository.py b/API/app/application/teams_repository.py index cc433d0..48cc633 100644 --- a/API/app/application/teams_repository.py +++ b/API/app/application/teams_repository.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Sequence from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -10,7 +10,28 @@ class TeamsRepository: def __init__(self, db: AsyncSession): self.db = db + async def get_all(self) -> Sequence[Team]: + stmt = select(Team) + result = await self.db.execute(stmt) + return result.scalars().all() + 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() + + async def create(self, team: Team) -> Team: + self.db.add(team) + await self.db.commit() + await self.db.refresh(team) + return team + + async def update(self, team: Team) -> Team: + await self.db.merge(team) + await self.db.commit() + return team + + async def delete(self, team: Team) -> Team: + await self.db.delete(team) + await self.db.commit() + return team diff --git a/API/app/application/users_repository.py b/API/app/application/users_repository.py index 0fa8373..ae283c9 100644 --- a/API/app/application/users_repository.py +++ b/API/app/application/users_repository.py @@ -15,10 +15,22 @@ class UsersRepository: result = await self.db.execute(stmt) return result.scalars().first() + async def get_by_id_with_role(self, user_id: int) -> Optional[User]: + stmt = ( + select(User) + .filter_by(id=user_id) + .options( + joinedload(User.profile).joinedload(Profile.role) + ) + ) + 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.profile)) ) result = await self.db.execute(stmt) return result.scalars().first() diff --git a/API/app/contollers/register_controller.py b/API/app/contollers/register_router.py similarity index 100% rename from API/app/contollers/register_controller.py rename to API/app/contollers/register_router.py diff --git a/API/app/contollers/teams_router.py b/API/app/contollers/teams_router.py new file mode 100644 index 0000000..23b961e --- /dev/null +++ b/API/app/contollers/teams_router.py @@ -0,0 +1,71 @@ +from typing import Optional + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database.session import get_db +from app.domain.entities.team import TeamEntity +from app.infrastructure.dependencies import get_current_user, require_admin +from app.infrastructure.teams_service import TeamsService + +router = APIRouter() + + +@router.get( + '/', + response_model=list[TeamEntity], + summary='Get all teams', + description='Returns all teams', +) +async def create_team( + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), +): + teams_service = TeamsService(db) + return await teams_service.get_all_teams() + + +@router.post( + '/', + response_model=Optional[TeamEntity], + summary='Create a new team', + description='Creates a new team', +) +async def create_team( + team: TeamEntity, + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), +): + teams_service = TeamsService(db) + return await teams_service.create_team(team) + + +@router.put( + '/{team_id}/', + response_model=Optional[TeamEntity], + summary='Update a team', + description='Updates a team', +) +async def create_team( + team_id: int, + team: TeamEntity, + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), +): + teams_service = TeamsService(db) + return await teams_service.update_team(team_id, team) + + +@router.delete( + '/{team_id}/', + response_model=Optional[TeamEntity], + summary='Delete a team', + description='Delete a team', +) +async def create_team( + team_id: int, + db: AsyncSession = Depends(get_db), + user=Depends(require_admin), +): + teams_service = TeamsService(db) + return await teams_service.delete_team(team_id) diff --git a/API/app/database/migrations/versions/d53be3a35511_0001_инициализация.py b/API/app/database/migrations/versions/d53be3a35511_0001_инициализация.py index d29f191..bb835f6 100644 --- a/API/app/database/migrations/versions/d53be3a35511_0001_инициализация.py +++ b/API/app/database/migrations/versions/d53be3a35511_0001_инициализация.py @@ -139,7 +139,7 @@ def upgrade() -> None: ) op.create_table('users', sa.Column('login', sa.VARCHAR(length=150), nullable=False), - sa.Column('password', sa.VARCHAR(), nullable=False), + sa.Column('password', sa.String(), nullable=False), sa.Column('profile_id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), diff --git a/API/app/domain/entities/team.py b/API/app/domain/entities/team.py new file mode 100644 index 0000000..c769bf6 --- /dev/null +++ b/API/app/domain/entities/team.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import BaseModel + +from app.domain.entities.profile import ProfileEntity + + +class TeamEntity(BaseModel): + id: Optional[int] = None + title: str + description: Optional[str] = None + logo: Optional[str] = None + git_url: Optional[str] = None + + profiles: Optional[list[ProfileEntity]] = None diff --git a/API/app/infrastructure/dependencies.py b/API/app/infrastructure/dependencies.py new file mode 100644 index 0000000..e7a6973 --- /dev/null +++ b/API/app/infrastructure/dependencies.py @@ -0,0 +1,43 @@ +import jwt +from fastapi import Depends, HTTPException, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.ext.asyncio import AsyncSession +from starlette import status + +from app.application.users_repository import UsersRepository +from app.database.session import get_db +from app.domain.models.users import User +from app.settings import get_auth_data + +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Security(security), + db: AsyncSession = Depends(get_db) +): + auth_data = get_auth_data() + + try: + payload = jwt.decode(credentials.credentials, auth_data["secret_key"], algorithms=[auth_data["algorithm"]]) + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + + user_id = payload.get("user_id") + if user_id is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + + user = await UsersRepository(db).get_by_id_with_role(user_id) + if user is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") + + return user + + +def require_admin(user: User = Depends(get_current_user)): + if user.profile.role.title != "Администратор": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied") + + return user diff --git a/API/app/infrastructure/teams_service.py b/API/app/infrastructure/teams_service.py new file mode 100644 index 0000000..dd703cc --- /dev/null +++ b/API/app/infrastructure/teams_service.py @@ -0,0 +1,75 @@ +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.application.teams_repository import TeamsRepository +from app.domain.entities.team import TeamEntity +from app.domain.models import Team + + +class TeamsService: + def __init__(self, db: AsyncSession): + self.teams_repository = TeamsRepository(db) + + async def get_all_teams(self) -> list[TeamEntity]: + teams = await self.teams_repository.get_all() + return [ + self.model_to_entity(team) + for team in teams + ] + + async def create_team(self, team: TeamEntity) -> Optional[TeamEntity]: + team_model = self.entity_to_model(team) + + await self.teams_repository.create(team_model) + + return self.model_to_entity(team_model) + + async def update_team(self, team_id: int, team: TeamEntity) -> Optional[TeamEntity]: + team_model = await self.teams_repository.get_by_id(team_id) + + if not team_model: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found") + + team_model.title = team.title + team_model.description = team_model.description + team_model.git_url = team_model.git_url + + await self.teams_repository.update(team_model) + + return self.model_to_entity(team_model) + + async def delete_team(self, team_id: int) -> Optional[TeamEntity]: + team_model = await self.teams_repository.get_by_id(team_id) + + if not team_model: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found") + + result = await self.teams_repository.delete(team_model) + + return self.model_to_entity(result) + + @staticmethod + def model_to_entity(team_model: Team) -> TeamEntity: + return TeamEntity( + id=team_model.id, + title=team_model.title, + description=team_model.description, + logo=team_model.logo, + git_url=team_model.git_url, + ) + + @staticmethod + def entity_to_model(team_entity: TeamEntity) -> Team: + team_model = Team( + title=team_entity.title, + description=team_entity.description, + logo=team_entity.logo, + git_url=team_entity.git_url, + ) + + if team_entity.id: + team_model.id = team_entity.id + + return team_model diff --git a/API/app/main.py b/API/app/main.py index cf1db0f..ccb2970 100644 --- a/API/app/main.py +++ b/API/app/main.py @@ -1,8 +1,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.contollers.register_controller import router as register_router +from app.contollers.register_router import router as register_router from app.contollers.auth_router import router as auth_router +from app.contollers.teams_router import router as team_router from app.settings import settings @@ -19,6 +20,7 @@ def start_app(): api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register']) api_app.include_router(auth_router, prefix=f'{settings.PREFIX}/auth', tags=['auth']) + api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams']) return api_app