Compare commits

..

8 Commits

Author SHA1 Message Date
IluaAir
88ace64d13 add priority filter 2025-09-30 22:19:43 +03:00
IluaAir
9decb7591e change admin dep 2025-09-30 22:09:26 +03:00
IluaAir
41c64127a5 del old task dep, page start from 1 2025-09-30 00:23:16 +03:00
IluaAir
ac28f9b420 add limit page date to date from 2025-09-30 00:01:13 +03:00
IluaAir
91daaf9275 delete protocol 2025-09-28 22:34:14 +03:00
IluaAir
64dcc77518 delete old dep 2025-09-28 22:20:34 +03:00
IluaAir
ddaa5b0c74 fix dep user for endpoints 2025-09-28 22:18:23 +03:00
IluaAir
23927e5347 add id tasks endpoint 2025-09-27 23:58:39 +03:00
12 changed files with 94 additions and 123 deletions

View File

@@ -5,9 +5,7 @@ from fastapi import Depends, Query
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
from src.schemas.tasks import TaskFilter from src.schemas.tasks import PriorityEnum, StatusEnum
TaskFilterDep = Annotated[TaskFilter, Depends()]
class Date(BaseModel): class Date(BaseModel):
@@ -23,4 +21,21 @@ class Date(BaseModel):
return self return self
DateDep = Annotated[Date, Depends()] class Page(BaseModel):
limit: int = Query(default=30, ge=1, le=100)
page: int | None = Query(default=1, ge=1)
class Status(BaseModel):
status: StatusEnum | None = Query(default=None)
class Priority(BaseModel):
priority: PriorityEnum | None = Query(default=None)
class Filters(Date, Status, Priority, Page):
pass
FilterDep = Annotated[Filters, Depends()]

View File

@@ -1,6 +1,6 @@
from typing import Annotated from typing import Annotated
from fastapi import Depends, HTTPException, Path from fastapi import Depends, HTTPException
from fastapi.security import ( from fastapi.security import (
HTTPAuthorizationCredentials, HTTPAuthorizationCredentials,
HTTPBearer, HTTPBearer,
@@ -8,17 +8,13 @@ from fastapi.security import (
) )
from jwt import InvalidTokenError from jwt import InvalidTokenError
from src.api.dependacies.db_dep import sessionDep
from src.core.auth_manager import AuthManager from src.core.auth_manager import AuthManager
from src.core.settings import settings from src.core.settings import settings
from src.schemas.auth import TokenData from src.schemas.auth import TokenData
from src.services.tasks import TaskService
from src.services.users import UserService
http_bearer = HTTPBearer(auto_error=False) http_bearer = HTTPBearer(auto_error=False)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
# AccessTokenDep = Annotated[str, Depends(oauth2_scheme)]
AccessTokenDep = Annotated[HTTPAuthorizationCredentials, Depends(http_bearer)] AccessTokenDep = Annotated[HTTPAuthorizationCredentials, Depends(http_bearer)]
@@ -54,48 +50,14 @@ async def get_current_user_for_refresh(token: AccessTokenDep):
return await get_current_user(token, verify_exp=False, check_active=True) return await get_current_user(token, verify_exp=False, check_active=True)
async def get_current_user_for_admin(token: AccessTokenDep):
admin = await get_current_user(token, verify_exp=True, check_active=True)
if not admin.is_superuser:
raise HTTPException(status_code=403, detail="Admin access required")
return admin
CurrentUser = Annotated[TokenData, Depends(get_current_user_basic)] CurrentUser = Annotated[TokenData, Depends(get_current_user_basic)]
ActiveUser = Annotated[TokenData, Depends(get_current_active_user)] ActiveUser = Annotated[TokenData, Depends(get_current_active_user)]
RefreshUser = Annotated[TokenData, Depends(get_current_user_for_refresh)] RefreshUser = Annotated[TokenData, Depends(get_current_user_for_refresh)]
AdminUser = Annotated[TokenData, Depends(get_current_user_for_admin)]
async def get_admin_user(db: sessionDep, current_user: ActiveUser):
await UserService(db).validate_admin_user(current_user.sub)
return current_user
AdminUser = Annotated[TokenData, Depends(get_admin_user)]
async def user_or_admin(db: sessionDep, current_user: ActiveUser, owner_id: int):
if current_user.id == owner_id:
return current_user
else:
admin = await get_admin_user(db, current_user)
return admin
async def CurrentOrAdminOwner(
db: sessionDep, current_user: ActiveUser, id: Annotated[int, Path()]
):
authorized_user = await user_or_admin(db, current_user, id)
if not authorized_user:
raise HTTPException(status_code=403, detail="Not authorized")
return authorized_user
async def CurrentOrAdminTask(
db: sessionDep,
id: Annotated[int, Path()],
current_user: ActiveUser,
):
task = await TaskService(db).get_task(id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return await CurrentOrAdminOwner(db, current_user, task.user_id)
OwnerDep = Annotated[TokenData, Depends(CurrentOrAdminOwner)]
TaskOwnerDep = Annotated[TokenData, Depends(CurrentOrAdminTask)]

View File

@@ -1,28 +1,20 @@
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Body, Depends from fastapi import APIRouter, Body, Depends, HTTPException
from src.api.dependacies.db_dep import sessionDep from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.task_dep import TaskFilterDep from src.api.dependacies.user_dep import ActiveUser
from src.api.dependacies.user_dep import ActiveUser, TaskOwnerDep
from src.schemas.tasks import TaskADDRequest, TaskPATCHRequest from src.schemas.tasks import TaskADDRequest, TaskPATCHRequest
from src.services.tasks import TaskService from src.services.tasks import TaskService
from src.services.users import UserService
router = APIRouter(prefix="/tasks", tags=["Tasks"]) router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
async def get_tasks(session: sessionDep, user: ActiveUser, filter: TaskFilterDep):
result = await UserService(session).get_user_with_tasks(
user_id=user.id, **filter.model_dump(exclude_unset=True)
)
return result
@router.get("/{id}") @router.get("/{id}")
async def get_task_id(session: sessionDep, id: int, _: TaskOwnerDep): async def get_task_id(session: sessionDep, id: int, user: ActiveUser):
task = await TaskService(session).get_task(id) task = await TaskService(session).get_task(id)
if task.user_id != user.id and user.is_superuser is False:
raise HTTPException(status_code=403, detail="Forbidden")
return task return task
@@ -42,17 +34,26 @@ async def post_task(
async def patch_task( async def patch_task(
session: sessionDep, session: sessionDep,
id: int, id: int,
_: TaskOwnerDep, user: ActiveUser,
task_data: TaskPATCHRequest = Body(), task_data: TaskPATCHRequest = Body(),
): ):
task = await TaskService(session).update_task(id, task_data) if user.is_superuser is False:
return task task = await TaskService(session).get_task(id)
if task.user_id != user.id:
raise HTTPException(status_code=403, detail="Forbidden")
updated_task = await TaskService(session).update_task(id, task_data)
return updated_task
@router.delete("/{id}") @router.delete("/{id}")
async def delete_task( async def delete_task(
session: sessionDep, session: sessionDep,
id: int, id: int,
_: TaskOwnerDep, user: ActiveUser,
): ):
if user.is_superuser is False:
task = await TaskService(session).get_task(id)
if task.user_id != user.id:
raise HTTPException(status_code=403, detail="Forbidden")
await TaskService(session).delete_task(id) await TaskService(session).delete_task(id)
return {"message": "Task deleted successfully"}

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter, Body from fastapi import APIRouter, Body, HTTPException
from src.api.dependacies.db_dep import sessionDep from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.task_dep import FilterDep
from src.api.dependacies.user_dep import ( from src.api.dependacies.user_dep import (
ActiveUser,
AdminUser, AdminUser,
OwnerDep,
) )
from src.core.settings import settings from src.core.settings import settings
from src.schemas.users import UserUpdate from src.schemas.users import UserUpdate
@@ -19,18 +20,30 @@ async def get_all_users(session: sessionDep, _: AdminUser):
@router.get("/{id}") @router.get("/{id}")
async def get_user_by_id(session: sessionDep, id: int, _: OwnerDep): async def get_user_by_id(session: sessionDep, id: int, _: AdminUser):
user = await UserService(session).get_user_by_filter_or_raise(id=id) user = await UserService(session).get_user_by_filter_or_raise(id=id)
return user return user
@router.get("/{id}/tasks")
async def get_user_tasks(
session: sessionDep, id: int, user: ActiveUser, filters: FilterDep
):
if user.id != id and user.is_superuser is False:
raise HTTPException(status_code=403, detail="Forbidden")
tasks = await UserService(session).get_user_with_tasks(id, **filters.model_dump())
return tasks.tasks
@router.patch("/{id}") @router.patch("/{id}")
async def patch_user( async def patch_user(
session: sessionDep, session: sessionDep,
id: int, id: int,
_: OwnerDep, user: ActiveUser,
user_update: UserUpdate = Body(), user_update: UserUpdate = Body(),
): ):
if user.id != id and user.is_superuser is False:
raise HTTPException(status_code=403, detail="Forbidden")
updated_user = await UserService(session).update_user( updated_user = await UserService(session).update_user(
id=id, update_data=user_update id=id, update_data=user_update
) )
@@ -38,6 +51,6 @@ async def patch_user(
@router.delete("/{id}") @router.delete("/{id}")
async def delete_user(session: sessionDep, id: int, _: AdminUser): async def delete_user(session: sessionDep, id: int, user: AdminUser):
await UserService(session).delete_user(id) await UserService(session).delete_user(id)
return {"message": "User deleted successfully"} return {"message": "User deleted successfully"}

View File

@@ -8,10 +8,6 @@ if TYPE_CHECKING:
from src.repository.users import UsersRepo from src.repository.users import UsersRepo
class HasId(Protocol):
id: Any
class IUOWDB(Protocol): class IUOWDB(Protocol):
session: AsyncSession session: AsyncSession
user: "UsersRepo" user: "UsersRepo"

View File

@@ -2,5 +2,5 @@ from src.models.tokens import RefreshTokensORM
from src.repository.base import BaseRepo from src.repository.base import BaseRepo
class AuthRepo(BaseRepo): class AuthRepo(BaseRepo[RefreshTokensORM]):
model: type[RefreshTokensORM] = RefreshTokensORM model: type[RefreshTokensORM] = RefreshTokensORM

View File

@@ -3,9 +3,7 @@ from typing import Any, Generic, Mapping, Sequence, Type, TypeVar
from sqlalchemy import delete, insert, select, update from sqlalchemy import delete, insert, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.core.interfaces import HasId ModelType = TypeVar("ModelType")
ModelType = TypeVar("ModelType", bound=HasId)
class BaseRepo(Generic[ModelType]): class BaseRepo(Generic[ModelType]):
@@ -45,12 +43,9 @@ class BaseRepo(Generic[ModelType]):
async def delete_one(self, **filter_by) -> None: async def delete_one(self, **filter_by) -> None:
await self.session.execute(delete(self.model).filter_by(**filter_by)) await self.session.execute(delete(self.model).filter_by(**filter_by))
async def update_one(self, id: int, data: dict[str, Any]) -> ModelType: async def update_one(self, data: dict[str, Any], **filter_by: Any) -> ModelType:
stmt = ( stmt = (
update(self.model) update(self.model).filter_by(**filter_by).values(data).returning(self.model)
.where(self.model.id == id)
.values(data)
.returning(self.model)
) )
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
model = result.scalar_one() model = result.scalar_one()

View File

@@ -2,5 +2,5 @@ from src.models.tasks import TasksORM
from src.repository.base import BaseRepo from src.repository.base import BaseRepo
class TasksRepo(BaseRepo): class TasksRepo(BaseRepo[TasksORM]):
model: type[TasksORM] = TasksORM model: type[TasksORM] = TasksORM

View File

@@ -1,5 +1,4 @@
from datetime import date from datetime import date
from typing import Optional
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -9,21 +8,24 @@ from src.models.tasks import TasksORM
from src.repository.base import BaseRepo from src.repository.base import BaseRepo
class UsersRepo(BaseRepo): class UsersRepo(BaseRepo[UsersORM]):
model: type[UsersORM] = UsersORM model: type[UsersORM] = UsersORM
async def get_one_with_load( async def get_one_with_load(
self, self,
user_id: int, user_id: int,
status: str | None = None, status: str | None = None,
tasks_limit: Optional[int] = None, priority: str | None = None,
tasks_offset: Optional[int] = 0, tasks_limit: int | None = None,
date_to: Optional[date] = None, tasks_offset: int | None = 0,
date_from: Optional[date] = None, date_to: date | None = None,
date_from: date | None = None,
) -> UsersORM | None: ) -> UsersORM | None:
filters_sq: dict = {"user_id": user_id} filters_sq: dict = {"user_id": user_id}
if status: if status:
filters_sq["status"] = status filters_sq["status"] = status
if priority:
filters_sq["priority"] = priority
tasks_subquery = self._tasks_subquary( tasks_subquery = self._tasks_subquary(
date_from=date_from, date_to=date_to, **filters_sq date_from=date_from, date_to=date_to, **filters_sq
) )

View File

@@ -50,11 +50,3 @@ class Task(TaskADDRequest):
time_spent: int time_spent: int
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
class TaskFilter(BaseModel):
status: StatusEnum | None = None
limit: int | None = 30
offset: int | None = None
date_from: date | None = None
date_to: date | None = None

View File

@@ -1,13 +1,10 @@
from fastapi import HTTPException from fastapi import HTTPException
from src.models.tasks import TasksORM
from src.schemas.tasks import Task, TaskADDRequest, TaskPATCHRequest from src.schemas.tasks import Task, TaskADDRequest, TaskPATCHRequest
from src.services.base import BaseService from src.services.base import BaseService
class TaskService(BaseService): class TaskService(BaseService):
model = TasksORM
async def create_task(self, user_id: int, task_data: TaskADDRequest) -> Task: async def create_task(self, user_id: int, task_data: TaskADDRequest) -> Task:
user = await self.session.user.get_one_or_none(id=user_id) user = await self.session.user.get_one_or_none(id=user_id)
if user is None: if user is None:
@@ -29,10 +26,13 @@ class TaskService(BaseService):
await self.session.commit() await self.session.commit()
async def update_task( async def update_task(
self, task_id: int, task_data: TaskPATCHRequest, exclude_unset: bool = True self,
task_id: int,
task_data: TaskPATCHRequest,
exclude_unset: bool = True,
): ):
task = await self.session.task.update_one( task = await self.session.task.update_one(
id=task_id, data=task_data.model_dump(exclude_unset=exclude_unset) data=task_data.model_dump(exclude_unset=exclude_unset), id=task_id
) )
await self.session.commit() await self.session.commit()
return Task.model_validate(task) return Task.model_validate(task)

View File

@@ -1,5 +1,3 @@
from datetime import date
from fastapi import HTTPException from fastapi import HTTPException
from src.schemas.users import User, UserUpdate, UserWithTasks from src.schemas.users import User, UserUpdate, UserWithTasks
@@ -36,27 +34,24 @@ class UserService(BaseService):
async def update_user(self, id: int, update_data: UserUpdate) -> User: async def update_user(self, id: int, update_data: UserUpdate) -> User:
await self.get_user_by_filter_or_raise(id=id) await self.get_user_by_filter_or_raise(id=id)
user = await self.session.user.update_one( user = await self.session.user.update_one(
id=id, data=update_data.model_dump(exclude_unset=True) data=update_data.model_dump(exclude_unset=True), id=id
) )
await self.session.commit() await self.session.commit()
return User.model_validate(user) return User.model_validate(user)
async def get_user_with_tasks( async def get_user_with_tasks(self, user_id: int, **attrs):
self, if attrs.get("page") and attrs.get("limit"):
user_id: int, tasks_offset = (attrs.get("page", 0) - 1) * attrs.get("limit")
status: str | None, else:
limit: int | None, tasks_offset = None
offset: int | None,
date_to: date | None,
date_from: date | None,
):
user = await self.session.user.get_one_with_load( user = await self.session.user.get_one_with_load(
user_id=user_id, user_id=user_id,
status=status, status=attrs.get("status"),
tasks_limit=limit, priority=attrs.get("priority"),
tasks_offset=offset, tasks_limit=attrs.get("limit"),
date_from=date_from, tasks_offset=tasks_offset,
date_to=date_to, date_from=attrs.get("date_from"),
date_to=attrs.get("date_to"),
) )
if user is None: if user is None:
raise HTTPException(status_code=404, detail="User not found.") raise HTTPException(status_code=404, detail="User not found.")