From 96d833a089cb146b50121d9c4a1e5ce70b71156d Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:20:19 +0100 Subject: [PATCH 01/14] Created schema projects_tasks to avoid circular imports --- schemas/projects.py | 4 +++- schemas/projects_tasks.py | 11 +++++++++++ schemas/tasks.py | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 schemas/projects_tasks.py diff --git a/schemas/projects.py b/schemas/projects.py index 6522337..302d7f5 100644 --- a/schemas/projects.py +++ b/schemas/projects.py @@ -7,12 +7,14 @@ from schemas.users import UserBase class ProjectBase(BaseModel): model_config = ConfigDict(from_attributes=True) - id: int name: str description: str tasks: List[TaskBase] users: List[UserBase] +class ProjectFull(ProjectBase): + id: int + class ProjectCreate(BaseModel): name: str description: Optional[str] = None diff --git a/schemas/projects_tasks.py b/schemas/projects_tasks.py new file mode 100644 index 0000000..9f97c28 --- /dev/null +++ b/schemas/projects_tasks.py @@ -0,0 +1,11 @@ +import schemas.projects as project_schemas +import schemas.tasks as task_schemas +from pydantic import ConfigDict + +class ProjectTaskBase(task_schemas.TaskBase): + model_config = ConfigDict(from_attributes=True) + + project: project_schemas.ProjectBase + +class ProjectTaskCreate(task_schemas.TaskCreate): + project: project_schemas.ProjectBase \ No newline at end of file diff --git a/schemas/tasks.py b/schemas/tasks.py index ea3b711..12f7d81 100644 --- a/schemas/tasks.py +++ b/schemas/tasks.py @@ -17,6 +17,11 @@ class TaskBase(BaseModel): description: Optional[str] = None status: TaskStatus = TaskStatus.PENDING +class TaskCreate(BaseModel): + title: str + description: Optional[str] = None + status: TaskStatus = TaskStatus.PENDING + class TaskUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None From 11899d985e5cf6f6dd82edf2b1269a870bf081be Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:25:37 +0100 Subject: [PATCH 02/14] Removed useless LoCs (unused imports, useless comments, etc.) --- routers/projects.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 226b26e..6f2b0d6 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -10,16 +10,13 @@ from database import db_dependency import schemas.tasks as tasks_schemas import schemas.projects as projects_schemas import schemas.users as users_schemas +import schemas.projects_tasks as projects_tasks_schemas from models import Project from routers.auth import get_user_from_jwt router = APIRouter(prefix="/projects", tags=["projects"]) -## -## GET endpoints -## - @router.get("/", response_model=List[projects_schemas.ProjectBase], tags=["projects", "me"]) def get_projects(db: db_dependency, request: Request): """Get a user's projects""" From 7bd232564957f27050cb993a5028f4aa54141f6b Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:26:17 +0100 Subject: [PATCH 03/14] Created more endpoints for project managment --- routers/projects.py | 165 ++++++++++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 66 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 6f2b0d6..7c32525 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -1,10 +1,6 @@ from fastapi import APIRouter, HTTPException, Depends, Request from typing import List, Annotated -from httpx import request -from jose import JWTError, jwt - -from routers import auth from database import db_dependency import schemas.tasks as tasks_schemas @@ -90,86 +86,123 @@ def create_project(project: projects_schemas.ProjectCreate, request:Request, db: return db_project -# """Get tasks from a specified project""" -# @router.get("/{project_id}/tasks", response_model=List[tasks_schemas.TaskBase], tags=["tasks"]) -# def read_tasks_from_project(project_id: int, db: db_dependency): -# db_tasks = db.query(tasks.models.Task).filter(tasks.models.Task.project_id == project_id).all() -# return db_tasks +@router.get("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskBase, tags=["tasks"]) +def read_task_from_project(project_id: int, task_id: int, db: db_dependency, request: Request): + """Get a specific task from a specified project""" + user = get_user_from_jwt(request, db) -# """Get a specific task from a specified project""" -# @router.get("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskBase, tags=["tasks"]) -# def read_task_from_project(project_id: int, task_id: int, db: db_dependency): -# db_task = db.query(tasks.models.Task).filter(tasks.models.Task.project_id == project_id, tasks.models.Task.id == task_id).first() -# if db_task is None: -# raise HTTPException(status_code=404, detail="Task not found in the specified project") -# return db_task + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") + db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found in the specified project") + return db_task -# """Get users from a specified project""" +@router.get("/{project_id}/users/{user_id}", response_model=users_schemas.UserBase, tags=["users"]) +def read_user_from_project(project_id: int, user_id: int, db: db_dependency, request: Request): + """Get a specific user from a specified project""" + user = get_user_from_jwt(request, db) -# @router.get("/{project_id}/users", response_model=List[users_schemas.UserBase], tags=["users"]) -# def read_users_from_project(project_id: int, db: db_dependency): -# db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() -# if db_project is None: -# raise HTTPException(status_code=404, detail="Project not found") -# return db_project.users + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's users") + db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + if db_user is None or db_user not in db_project.users: + raise HTTPException(status_code=404, detail="User not found in the specified project") + return db_user -# ## -# ## POST endpoints -# ## +@router.post("/{project_id}/tasks", response_model=projects_tasks_schemas.ProjectTaskCreate, tags=["tasks"]) +def create_task_in_project(project_id: int, task: tasks_schemas.TaskCreate, db: db_dependency, request: Request): + """Create a new task in a specified project""" + user = get_user_from_jwt(request, db) + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to add tasks to this project") -# """Create a new project""" + db_task = projects_tasks_schemas.ProjectTaskCreate( + title=task.title, + description=task.description, + status=task.status, + project=db_project + ) + + db.add(db_task) + db.commit() + db.refresh(db_task) + return db_task -# @router.post("/", response_model=projects.ProjectCreate) -# def create_project(project: projects.ProjectCreate, db: db_dependency): -# db_project = projects( -# name=project.name, -# description=project.description -# ) -# db.add(db_project) -# db.commit() -# db.refresh(db_project) -# return db_project +@router.post("/{project_id}/users", response_model=projects_schemas.ProjectAddUsers, tags=["users"]) +def add_users_to_project(project_id: int, user_data: projects_schemas.ProjectAddUsers, db: db_dependency, request: Request): + """Add users to a specified project using their IDs""" + user = get_user_from_jwt(request, db) + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") -# """Create a new task in a specified project""" + for user_id in user_data.user_ids: + db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + if db_user: + db_project.users.append(db_user) + db.commit() + db.refresh(db_project) + return db_project -# @router.post("/{project_id}/tasks", response_model=tasks.TaskBase, tags=["tasks"]) -# def create_task_in_project(project_id: int, task: tasks.TaskBase, db: db_dependency): -# db_task = tasks.models.Task( -# title=task.title, -# description=task.description, -# status=task.status, -# project_id=project_id -# ) -# db.add(db_task) -# db.commit() -# db.refresh(db_task) -# return db_task +@router.delete("/{project_id}/users/{user_id}", response_model=projects_schemas.ProjectRemoveUsers, tags=["users"]) +def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, request: Request): + """Remove a user from a specified project using their ID""" + user = get_user_from_jwt(request, db) + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") -# """Add users to a specified project using their IDs""" + db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + if db_user is None or db_user not in db_project.users: + raise HTTPException(status_code=404, detail="User not found in the specified project") -# @router.post("/{project_id}/users", response_model=projects.ProjectAddUsers, tags=["users"]) -# def add_users_to_project(project_id: int, user_data: projects.ProjectAddUsers, db: db_dependency): -# db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() -# if db_project is None: -# raise HTTPException(status_code=404, detail="Project not found") -# for user_id in user_data.user_ids: -# db_user = db.query(users.models.User).filter(users.models.User.id == user_id).first() -# if db_user: -# db_project.users.append(db_user) -# db.commit() -# db.refresh(db_project) -# return db_project + db_project.users.remove(db_user) + db.commit() + db.refresh(db_project) + return db_project +@router.put("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskUpdate, tags=["tasks"]) +def update_task_in_project(project_id: int, task_id: int, task: tasks_schemas.TaskUpdate, db: db_dependency, request: Request): + """Update a task in a specified project""" + user = get_user_from_jwt(request, db) -# ## -# ## PUT endpoints -# ## + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") + db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found in the specified project") + if task.title is not None: + db_task.title = task.title + if task.description is not None: + db_task.description = task.description + if task.status is not None: + db_task.status = task.status + db.commit() + db.refresh(db_task) + return db_task # """Update a project by ID""" From 3580a4f79f4fc3075cf598dba92c23dbdebfe0e2 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:30:41 +0100 Subject: [PATCH 04/14] Created more endpoints for project managment --- routers/projects.py | 113 +++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 69 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 7c32525..853a600 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -204,78 +204,53 @@ def update_task_in_project(project_id: int, task_id: int, task: tasks_schemas.Ta db.refresh(db_task) return db_task -# """Update a project by ID""" +@router.put("/{project_id}", response_model=projects_schemas.ProjectUpdate) +def update_project(project_id: int, project: projects_schemas.ProjectUpdate, db: db_dependency, request: Request): + """Update a project by ID""" + user = get_user_from_jwt(request, db) -# @router.put("/{project_id}", response_model=projects.ProjectUpdate) -# def update_project(project_id: int, project: projects.ProjectUpdate, db: db_dependency): -# db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() -# if db_project is None: -# raise HTTPException(status_code=404, detail="Project not found") -# if project.name is not None: -# db_project.name = project.name -# if project.description is not None: -# db_project.description = project.description -# db.commit() -# db.refresh(db_project) -# return db_project + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to modify this project") + if project.name is not None: + db_project.name = project.name + if project.description is not None: + db_project.description = project.description + db.commit() + db.refresh(db_project) + return db_project +@router.delete("/{project_id}", tags=["projects"]) +def delete_project(project_id: int, db: db_dependency, request: Request): + """Delete a project by ID""" + user = get_user_from_jwt(request, db) -# """Update a task in a specified project""" + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to delete this project") + + db.delete(db_project) + db.commit() + return {"detail": "Project deleted successfully"} -# @router.put("/{project_id}/tasks/{task_id}", response_model=tasks.TaskUpdate, tags=["tasks"]) -# def update_task_in_project(project_id: int, task_id: int, task: tasks.TaskUpdate, db: db_dependency): -# db_task = db.query(tasks.models.Task).filter(tasks.models.Task.project_id == project_id, tasks.models.Task.id == task_id).first() -# if db_task is None: -# raise HTTPException(status_code=404, detail="Task not found in the specified project") -# if task.title is not None: -# db_task.title = task.title -# if task.description is not None: -# db_task.description = task.description -# if task.status is not None: -# db_task.status = task.status -# db.commit() -# db.refresh(db_task) -# return db_task +@router.delete("/{project_id}/tasks/{task_id}" , tags=["tasks"]) +def delete_task_from_project(project_id: int, task_id: int, db: db_dependency, request: Request): + """Delete a task from a specified project""" + user = get_user_from_jwt(request, db) -# ## -# ## DELETE endpoints -# ## + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") -# """Delete a project by ID""" - -# @router.delete("/{project_id}") -# def delete_project(project_id: int, db: db_dependency): -# db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() -# if db_project is None: -# raise HTTPException(status_code=404, detail="Project not found") -# db.delete(db_project) -# db.commit() -# return {"detail": "Project deleted successfully"} - - -# """Delete a task from a specified project""" - -# @router.delete("/{project_id}/tasks/{task_id}" , tags=["tasks"]) -# def delete_task_from_project(project_id: int, task_id: int, db: db_dependency): -# db_task = db.query(tasks.models.Task).filter(tasks.models.Task.project_id == project_id, tasks.models.Task.id == task_id).first() -# if db_task is None: -# raise HTTPException(status_code=404, detail="Task not found in the specified project") -# db.delete(db_task) -# db.commit() -# return {"detail": "Task deleted successfully"} - - -# """Remove users from a specified project using their IDs""" - -# @router.delete("/{project_id}/users/{user_id}", tags=["users"]) -# def remove_user_from_project(project_id: int, user_id: int, db: db_dependency): -# db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() -# if db_project is None: -# raise HTTPException(status_code=404, detail="Project not found") -# db_user = db.query(users.models.User).filter(users.models.User.id == user_id).first() -# if db_user is None or db_user not in db_project.users: -# raise HTTPException(status_code=404, detail="User not found in the specified project") -# db_project.users.remove(db_user) -# db.commit() -# db.refresh(db_project) -# return {"detail": "User removed from project successfully"} + db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found in the specified project") + db.delete(db_task) + db.commit() + return {"detail": "Task deleted successfully"} From 5f7746279b0336cd4cdef8897955a1409d9e7eff Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:41:53 +0100 Subject: [PATCH 05/14] More consistent endpoint methods and ordered them --- routers/projects.py | 72 +++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 853a600..24244b2 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -52,6 +52,39 @@ def get_project_users(project_id: int, request:Request, db: db_dependency): return db_project.users + +@router.get("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskBase, tags=["tasks"]) +def get_project_task(project_id: int, task_id: int, db: db_dependency, request: Request): + """Get a specific task from a specified project""" + user = get_user_from_jwt(request, db) + + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") + + db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found in the specified project") + return db_task + +@router.get("/{project_id}/users/{user_id}", response_model=users_schemas.UserBase, tags=["users"]) +def get_project_user(project_id: int, user_id: int, db: db_dependency, request: Request): + """Get a specific user from a specified project""" + user = get_user_from_jwt(request, db) + + db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project's users") + + db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + if db_user is None or db_user not in db_project.users: + raise HTTPException(status_code=404, detail="User not found in the specified project") + return db_user + @router.get("/{project_id}/tasks", response_model=List[tasks_schemas.TaskBase], tags=["tasks", "projects"]) def get_project_tasks(project_id: int, request:Request, db: db_dependency): """Get tasks from a specified project""" @@ -86,40 +119,9 @@ def create_project(project: projects_schemas.ProjectCreate, request:Request, db: return db_project -@router.get("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskBase, tags=["tasks"]) -def read_task_from_project(project_id: int, task_id: int, db: db_dependency, request: Request): - """Get a specific task from a specified project""" - user = get_user_from_jwt(request, db) - - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - - db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() - if db_task is None: - raise HTTPException(status_code=404, detail="Task not found in the specified project") - return db_task - -@router.get("/{project_id}/users/{user_id}", response_model=users_schemas.UserBase, tags=["users"]) -def read_user_from_project(project_id: int, user_id: int, db: db_dependency, request: Request): - """Get a specific user from a specified project""" - user = get_user_from_jwt(request, db) - - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's users") - - db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() - if db_user is None or db_user not in db_project.users: - raise HTTPException(status_code=404, detail="User not found in the specified project") - return db_user @router.post("/{project_id}/tasks", response_model=projects_tasks_schemas.ProjectTaskCreate, tags=["tasks"]) -def create_task_in_project(project_id: int, task: tasks_schemas.TaskCreate, db: db_dependency, request: Request): +def create_project_task(project_id: int, task: tasks_schemas.TaskCreate, db: db_dependency, request: Request): """Create a new task in a specified project""" user = get_user_from_jwt(request, db) @@ -142,7 +144,7 @@ def create_task_in_project(project_id: int, task: tasks_schemas.TaskCreate, db: return db_task @router.post("/{project_id}/users", response_model=projects_schemas.ProjectAddUsers, tags=["users"]) -def add_users_to_project(project_id: int, user_data: projects_schemas.ProjectAddUsers, db: db_dependency, request: Request): +def add_project_user(project_id: int, user_data: projects_schemas.ProjectAddUsers, db: db_dependency, request: Request): """Add users to a specified project using their IDs""" user = get_user_from_jwt(request, db) @@ -181,7 +183,7 @@ def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, r return db_project @router.put("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskUpdate, tags=["tasks"]) -def update_task_in_project(project_id: int, task_id: int, task: tasks_schemas.TaskUpdate, db: db_dependency, request: Request): +def update_project_task(project_id: int, task_id: int, task: tasks_schemas.TaskUpdate, db: db_dependency, request: Request): """Update a task in a specified project""" user = get_user_from_jwt(request, db) @@ -238,7 +240,7 @@ def delete_project(project_id: int, db: db_dependency, request: Request): return {"detail": "Project deleted successfully"} @router.delete("/{project_id}/tasks/{task_id}" , tags=["tasks"]) -def delete_task_from_project(project_id: int, task_id: int, db: db_dependency, request: Request): +def delete_project_task(project_id: int, task_id: int, db: db_dependency, request: Request): """Delete a task from a specified project""" user = get_user_from_jwt(request, db) From 2a9fdc31a6a73004e2f9a13cdd81c70fffa08659 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:51:30 +0100 Subject: [PATCH 06/14] Refactored routers/projects.py to make way more readable by changing imports --- routers/projects.py | 79 +++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 24244b2..05c27f2 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -3,17 +3,18 @@ from typing import List, Annotated from database import db_dependency -import schemas.tasks as tasks_schemas -import schemas.projects as projects_schemas -import schemas.users as users_schemas -import schemas.projects_tasks as projects_tasks_schemas +from schemas.tasks import TaskBase, TaskCreate, TaskUpdate +from schemas.projects import ProjectBase, ProjectCreate, ProjectUpdate, ProjectAddUsers, ProjectRemoveUsers +from schemas.users import UserBase +from schemas.projects_users import ProjectUserBase +from schemas.projects_tasks import ProjectTaskBase, ProjectTaskCreate from models import Project from routers.auth import get_user_from_jwt router = APIRouter(prefix="/projects", tags=["projects"]) -@router.get("/", response_model=List[projects_schemas.ProjectBase], tags=["projects", "me"]) +@router.get("/", response_model=List[ProjectBase], tags=["projects", "me"]) def get_projects(db: db_dependency, request: Request): """Get a user's projects""" @@ -21,11 +22,11 @@ def get_projects(db: db_dependency, request: Request): user_id = getattr(user, "id") ## fetching projects for the user - projects = db.query(Project).join(Project.users).filter(getattr(users_schemas.UserBase, "id") == int(user_id)).all() + projects = db.query(Project).join(Project.users).filter(getattr(UserBase, "id") == int(user_id)).all() return projects -@router.get("/{project_id}", response_model=projects_schemas.ProjectBase) +@router.get("/{project_id}", response_model=ProjectBase) def get_project(project_id: int, request:Request, db: db_dependency): """Get a project by ID""" @@ -39,7 +40,7 @@ def get_project(project_id: int, request:Request, db: db_dependency): return db_project -@router.get("/{project_id}/users", response_model=List[users_schemas.UserBase], tags=["users", "projects"]) +@router.get("/{project_id}/users", response_model=List[UserBase], tags=["users", "projects"]) def get_project_users(project_id: int, request:Request, db: db_dependency): """Get users from a specified project""" @@ -53,7 +54,7 @@ def get_project_users(project_id: int, request:Request, db: db_dependency): return db_project.users -@router.get("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskBase, tags=["tasks"]) +@router.get("/{project_id}/tasks/{task_id}", response_model=TaskBase, tags=["tasks"]) def get_project_task(project_id: int, task_id: int, db: db_dependency, request: Request): """Get a specific task from a specified project""" user = get_user_from_jwt(request, db) @@ -64,12 +65,12 @@ def get_project_task(project_id: int, task_id: int, db: db_dependency, request: if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() if db_task is None: raise HTTPException(status_code=404, detail="Task not found in the specified project") return db_task -@router.get("/{project_id}/users/{user_id}", response_model=users_schemas.UserBase, tags=["users"]) +@router.get("/{project_id}/users/{user_id}", response_model=UserBase, tags=["users"]) def get_project_user(project_id: int, user_id: int, db: db_dependency, request: Request): """Get a specific user from a specified project""" user = get_user_from_jwt(request, db) @@ -80,31 +81,31 @@ def get_project_user(project_id: int, user_id: int, db: db_dependency, request: if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to access this project's users") - db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() if db_user is None or db_user not in db_project.users: raise HTTPException(status_code=404, detail="User not found in the specified project") return db_user -@router.get("/{project_id}/tasks", response_model=List[tasks_schemas.TaskBase], tags=["tasks", "projects"]) +@router.get("/{project_id}/tasks", response_model=List[TaskBase], tags=["tasks", "projects"]) def get_project_tasks(project_id: int, request:Request, db: db_dependency): """Get tasks from a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - return db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id).all() + return db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id).all() -@router.post("/", response_model=projects_schemas.ProjectCreate) -def create_project(project: projects_schemas.ProjectCreate, request:Request, db: db_dependency): +@router.post("/", response_model=ProjectCreate) +def create_project(project: ProjectCreate, request:Request, db: db_dependency): """Create a new project""" user = get_user_from_jwt(request, db) - db_project = projects_schemas.ProjectCreate( + db_project = ProjectCreate( name=project.name, description=project.description, tasks=[], @@ -120,18 +121,18 @@ def create_project(project: projects_schemas.ProjectCreate, request:Request, db: return db_project -@router.post("/{project_id}/tasks", response_model=projects_tasks_schemas.ProjectTaskCreate, tags=["tasks"]) -def create_project_task(project_id: int, task: tasks_schemas.TaskCreate, db: db_dependency, request: Request): +@router.post("/{project_id}/tasks", response_model=ProjectTaskCreate, tags=["tasks"]) +def create_project_task(project_id: int, task: TaskCreate, db: db_dependency, request: Request): """Create a new task in a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to add tasks to this project") - db_task = projects_tasks_schemas.ProjectTaskCreate( + db_task = ProjectTaskCreate( title=task.title, description=task.description, status=task.status, @@ -143,37 +144,37 @@ def create_project_task(project_id: int, task: tasks_schemas.TaskCreate, db: db_ db.refresh(db_task) return db_task -@router.post("/{project_id}/users", response_model=projects_schemas.ProjectAddUsers, tags=["users"]) -def add_project_user(project_id: int, user_data: projects_schemas.ProjectAddUsers, db: db_dependency, request: Request): +@router.post("/{project_id}/users", response_model=ProjectAddUsers, tags=["users"]) +def add_project_user(project_id: int, user_data: ProjectAddUsers, db: db_dependency, request: Request): """Add users to a specified project using their IDs""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") for user_id in user_data.user_ids: - db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() if db_user: db_project.users.append(db_user) db.commit() db.refresh(db_project) return db_project -@router.delete("/{project_id}/users/{user_id}", response_model=projects_schemas.ProjectRemoveUsers, tags=["users"]) +@router.delete("/{project_id}/users/{user_id}", response_model=ProjectRemoveUsers, tags=["users"]) def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, request: Request): """Remove a user from a specified project using their ID""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") - db_user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() if db_user is None or db_user not in db_project.users: raise HTTPException(status_code=404, detail="User not found in the specified project") @@ -182,18 +183,18 @@ def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, r db.refresh(db_project) return db_project -@router.put("/{project_id}/tasks/{task_id}", response_model=tasks_schemas.TaskUpdate, tags=["tasks"]) -def update_project_task(project_id: int, task_id: int, task: tasks_schemas.TaskUpdate, db: db_dependency, request: Request): +@router.put("/{project_id}/tasks/{task_id}", response_model=TaskUpdate, tags=["tasks"]) +def update_project_task(project_id: int, task_id: int, task: TaskUpdate, db: db_dependency, request: Request): """Update a task in a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() if db_task is None: raise HTTPException(status_code=404, detail="Task not found in the specified project") if task.title is not None: @@ -206,12 +207,12 @@ def update_project_task(project_id: int, task_id: int, task: tasks_schemas.TaskU db.refresh(db_task) return db_task -@router.put("/{project_id}", response_model=projects_schemas.ProjectUpdate) -def update_project(project_id: int, project: projects_schemas.ProjectUpdate, db: db_dependency, request: Request): +@router.put("/{project_id}", response_model=ProjectUpdate) +def update_project(project_id: int, project: ProjectUpdate, db: db_dependency, request: Request): """Update a project by ID""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: @@ -229,7 +230,7 @@ def delete_project(project_id: int, db: db_dependency, request: Request): """Delete a project by ID""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: @@ -244,13 +245,13 @@ def delete_project_task(project_id: int, task_id: int, db: db_dependency, reques """Delete a task from a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - db_task = db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.TaskBase, "project_id") == project_id, getattr(tasks_schemas.TaskBase, "id") == task_id).first() + db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() if db_task is None: raise HTTPException(status_code=404, detail="Task not found in the specified project") db.delete(db_task) From e53fb3f7731bfe626a3f9cd1cfa6f0e884505b3c Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 12:25:22 +0100 Subject: [PATCH 07/14] Refactored routers/projects.py to cut down on per-endpoint LoCs using helper methods --- routers/projects.py | 124 +++++++++++++++----------------------------- 1 file changed, 43 insertions(+), 81 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 05c27f2..9a39948 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -11,6 +11,26 @@ from schemas.projects_tasks import ProjectTaskBase, ProjectTaskCreate from models import Project from routers.auth import get_user_from_jwt + +def get_project_by_id_for_user(user: UserBase, project_id: int, db: db_dependency) -> ProjectBase: + """Get a project by ID and verify user has access""" + db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + if user not in db_project.users: + raise HTTPException(status_code=403, detail="Not authorized to access this project") + + return db_project + +def get_task_by_id_for_project(project: ProjectBase, task_id: int, db: db_dependency) -> TaskBase: + """ + Get a task by ID within a project + Supposes the user has already been verified to have access to the project + """ + db_task = db.query(TaskBase).filter(getattr(TaskBase, "id") == task_id, getattr(TaskBase, "project_id") == getattr(project, "id")).first() + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found in the specified project") + return db_task router = APIRouter(prefix="/projects", tags=["projects"]) @@ -31,43 +51,25 @@ def get_project(project_id: int, request:Request, db: db_dependency): """Get a project by ID""" user = get_user_from_jwt(request, db) - - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project") - - return db_project + return get_project_by_id_for_user(user, project_id, db) @router.get("/{project_id}/users", response_model=List[UserBase], tags=["users", "projects"]) def get_project_users(project_id: int, request:Request, db: db_dependency): """Get users from a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's users") - + db_project = get_project_by_id_for_user(user, project_id, db) return db_project.users @router.get("/{project_id}/tasks/{task_id}", response_model=TaskBase, tags=["tasks"]) def get_project_task(project_id: int, task_id: int, db: db_dependency, request: Request): """Get a specific task from a specified project""" + user = get_user_from_jwt(request, db) + db_project = get_project_by_id_for_user(user, project_id, db) + db_task = get_task_by_id_for_project(db_project, task_id, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - - db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() - if db_task is None: - raise HTTPException(status_code=404, detail="Task not found in the specified project") return db_task @router.get("/{project_id}/users/{user_id}", response_model=UserBase, tags=["users"]) @@ -75,11 +77,7 @@ def get_project_user(project_id: int, user_id: int, db: db_dependency, request: """Get a specific user from a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's users") + db_project : ProjectBase = get_project_by_id_for_user(user, project_id, db) db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() if db_user is None or db_user not in db_project.users: @@ -91,20 +89,16 @@ def get_project_tasks(project_id: int, request:Request, db: db_dependency): """Get tasks from a specified project""" user = get_user_from_jwt(request, db) - - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - - return db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id).all() + db_project = get_project_by_id_for_user(user, project_id, db) + db_tasks = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id).all() + return db_tasks @router.post("/", response_model=ProjectCreate) def create_project(project: ProjectCreate, request:Request, db: db_dependency): """Create a new project""" user = get_user_from_jwt(request, db) + db_project = ProjectCreate( name=project.name, description=project.description, @@ -126,11 +120,7 @@ def create_project_task(project_id: int, task: TaskCreate, db: db_dependency, re """Create a new task in a specified project""" user = get_user_from_jwt(request, db) - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to add tasks to this project") + db_project = get_project_by_id_for_user(user, project_id, db) db_task = ProjectTaskCreate( title=task.title, @@ -149,11 +139,7 @@ def add_project_user(project_id: int, user_data: ProjectAddUsers, db: db_depende """Add users to a specified project using their IDs""" user = get_user_from_jwt(request, db) - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") + db_project = get_project_by_id_for_user(user, project_id, db) for user_id in user_data.user_ids: db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() @@ -168,11 +154,7 @@ def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, r """Remove a user from a specified project using their ID""" user = get_user_from_jwt(request, db) - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to modify this project's users") + db_project = get_project_by_id_for_user(user, project_id, db) db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() if db_user is None or db_user not in db_project.users: @@ -187,22 +169,16 @@ def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, r def update_project_task(project_id: int, task_id: int, task: TaskUpdate, db: db_dependency, request: Request): """Update a task in a specified project""" user = get_user_from_jwt(request, db) + db_project = get_project_by_id_for_user(user, project_id, db) + db_task = get_task_by_id_for_project(db_project, task_id, db) - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - - db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() - if db_task is None: - raise HTTPException(status_code=404, detail="Task not found in the specified project") if task.title is not None: db_task.title = task.title if task.description is not None: db_task.description = task.description if task.status is not None: db_task.status = task.status + db.commit() db.refresh(db_task) return db_task @@ -212,15 +188,13 @@ def update_project(project_id: int, project: ProjectUpdate, db: db_dependency, r """Update a project by ID""" user = get_user_from_jwt(request, db) - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to modify this project") + db_project = get_project_by_id_for_user(user, project_id, db) + if project.name is not None: db_project.name = project.name if project.description is not None: db_project.description = project.description + db.commit() db.refresh(db_project) return db_project @@ -229,12 +203,7 @@ def update_project(project_id: int, project: ProjectUpdate, db: db_dependency, r def delete_project(project_id: int, db: db_dependency, request: Request): """Delete a project by ID""" user = get_user_from_jwt(request, db) - - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to delete this project") + db_project = get_project_by_id_for_user(user, project_id, db) db.delete(db_project) db.commit() @@ -244,16 +213,9 @@ def delete_project(project_id: int, db: db_dependency, request: Request): def delete_project_task(project_id: int, task_id: int, db: db_dependency, request: Request): """Delete a task from a specified project""" user = get_user_from_jwt(request, db) - - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - if user not in db_project.users: - raise HTTPException(status_code=403, detail="Not authorized to access this project's tasks") - - db_task = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id, getattr(TaskBase, "id") == task_id).first() - if db_task is None: - raise HTTPException(status_code=404, detail="Task not found in the specified project") + db_project = get_project_by_id_for_user(user, project_id, db) + db_task = get_task_by_id_for_project(db_project, task_id, db) + db.delete(db_task) db.commit() return {"detail": "Task deleted successfully"} From 52f13f5023fb26009cf961147a945ba471668660 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 12:33:20 +0100 Subject: [PATCH 08/14] fixed oversight where deleting a project would leave dangling tasks and user-to-project relations --- routers/projects.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/routers/projects.py b/routers/projects.py index 9a39948..b694d3c 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -205,6 +205,17 @@ def delete_project(project_id: int, db: db_dependency, request: Request): user = get_user_from_jwt(request, db) db_project = get_project_by_id_for_user(user, project_id, db) + ## Remove dangling tasks and user associations + tasks: List[TaskBase] = db.query(TaskBase).filter(getattr(TaskBase, "project_id") == project_id).all() + for task in tasks: + db.delete(task) + db_project.tasks.remove(task) + + users: List[ProjectUserBase] = db.query(ProjectUserBase).join(ProjectUserBase).filter(getattr(ProjectBase, "id") == project_id).all() + for proj_user in users: + db_project.users.remove(proj_user) + proj_user.projects.remove(db_project) + db.delete(db_project) db.commit() return {"detail": "Project deleted successfully"} @@ -217,5 +228,7 @@ def delete_project_task(project_id: int, task_id: int, db: db_dependency, reques db_task = get_task_by_id_for_project(db_project, task_id, db) db.delete(db_task) + db_project.tasks.remove(db_task) + db.commit() return {"detail": "Task deleted successfully"} From a3fb4903edef5db4d1f100af94e733063516f81d Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 12:34:56 +0100 Subject: [PATCH 09/14] Fixed similar to last oversight where deleting a user would let it still in projects assigned users --- routers/me.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/routers/me.py b/routers/me.py index f06bb9c..44f78fe 100644 --- a/routers/me.py +++ b/routers/me.py @@ -35,6 +35,18 @@ def delete_me(request: Request, db: db_dependency): """Delete current authenticated user""" user = auth.get_user_from_jwt(request, db) + + ## Remove user from all projects, delete projects with no users left + projects = user.projects[:] + for project in projects: + project.users.remove(user) + if len(project.users) == 0: + ## delete project if no users left + tasks = project.tasks[:] + for task in tasks: + db.delete(task) + db.delete(project) + db.delete(user) db.commit() ## Logout user by clearing cookie From b34662e877862bc94e2c07444d622c29ad52fa89 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 13:05:10 +0100 Subject: [PATCH 10/14] refactored auth.py to use helper methods --- routers/auth.py | 97 +++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/routers/auth.py b/routers/auth.py index b6c8586..1264294 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -18,7 +18,7 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours def create_access_token(data: dict, expires_delta: timedelta | None = None): """Create a JWT token""" - + to_encode = data.copy() if expires_delta: expire = datetime.now(timezone.utc) + expires_delta @@ -29,38 +29,63 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt +def verify_jwt_token(token: str): + """Verify and decode a JWT token""" + + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + ) + return user_id + + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + ) + def get_user_from_jwt(request: Request, db: db_dependency) -> models.User : """Helper function to check for valid JWT token in cookies""" - token = request.cookies.get("access_token") - if not token: + + get_token = request.cookies.get("access_token") + + if not get_token or get_token is None: raise HTTPException( status_code=401, detail="Not logged in" ) try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=401, - detail="Not logged in" - ) + user_id: str = verify_jwt_token(get_token) ## verifying token validity + db_user = db.query(models.User).filter(models.User.id == int(user_id)).first() if db_user is None: request.cookies.clear() ## removing invalid auth cookie raise HTTPException( status_code=401, - detail="User not found" + detail="Could not verify credentials" ) return db_user - - except JWTError: + except HTTPException: request.cookies.clear() ## removing invalid auth cookie + raise + +def verify_user_password(user_id: int, password: str, db: db_dependency) -> None: + """Verify user's password""" + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if db_user is None: raise HTTPException( status_code=401, - detail="Could not validate credentials" - ) + detail="Could not verify credentials") + + hashed_password = hash(password=password, salt=str(getattr(db_user,"password_salt")), variant="id") + if hashed_password != db_user.password_hash: + raise HTTPException( + status_code=401, + detail="Could not verify credentials") @router.post("/login") def login(user_data: user_schemas.UserLogin, request: Request, response: Response, db: db_dependency): @@ -78,8 +103,9 @@ def login(user_data: user_schemas.UserLogin, request: Request, response: Respons } } except HTTPException: - pass # Token invalid or expired, proceed to login + request.cookies.clear() ## removing invalid auth cookie + ## check if user exists db_user = db.query(models.User).filter(models.User.email == user_data.email).first() if db_user is None: raise HTTPException( @@ -87,16 +113,12 @@ def login(user_data: user_schemas.UserLogin, request: Request, response: Respons detail="Incorrect email or password" ) - if not verify_user_password(getattr(db_user, "id"), user_data.password, db): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password" - ) + verify_user_password(getattr(db_user, "id"), user_data.password, db) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( - data={"sub": str(db_user.id)}, expires_delta=access_token_expires - + data={"sub": str(db_user.id)}, + expires_delta=access_token_expires ) # Set JWT in httpOnly cookie @@ -118,30 +140,3 @@ def login(user_data: user_schemas.UserLogin, request: Request, response: Respons } } - -def verify_jwt_token(token: str): - """Verify and decode a JWT token""" - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id = payload.get("sub") - if user_id is None: - raise credentials_exception - return user_id - except JWTError: - raise credentials_exception - -def verify_user_password(user_id: int, password: str, db: db_dependency) -> bool: - """Verify user's password""" - db_user = db.query(models.User).filter(models.User.id == user_id).first() - if db_user is None: - return False - - hashed_password = hash(password=password, salt=str(getattr(db_user,"password_salt")), variant="id") - if hashed_password != db_user.password_hash: - return False - - return True \ No newline at end of file From e6e285a2c176c8abef954f2eae12c380b143c8b5 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 13:19:15 +0100 Subject: [PATCH 11/14] fixed some routers/projects.py get endpoint where Schemas and models where confused between themselves. --- routers/projects.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index b694d3c..874c483 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -9,12 +9,12 @@ from schemas.users import UserBase from schemas.projects_users import ProjectUserBase from schemas.projects_tasks import ProjectTaskBase, ProjectTaskCreate -from models import Project +from models import Project, Task, User from routers.auth import get_user_from_jwt def get_project_by_id_for_user(user: UserBase, project_id: int, db: db_dependency) -> ProjectBase: """Get a project by ID and verify user has access""" - db_project = db.query(ProjectBase).filter(getattr(ProjectBase, "id") == project_id).first() + db_project = db.query(Project).filter(Project.id == project_id).first() if db_project is None: raise HTTPException(status_code=404, detail="Project not found") if user not in db_project.users: @@ -27,7 +27,7 @@ def get_task_by_id_for_project(project: ProjectBase, task_id: int, db: db_depend Get a task by ID within a project Supposes the user has already been verified to have access to the project """ - db_task = db.query(TaskBase).filter(getattr(TaskBase, "id") == task_id, getattr(TaskBase, "project_id") == getattr(project, "id")).first() + db_task = db.query(Task).filter(Task.id == task_id, Task.project_id == getattr(project, "id")).first() if db_task is None: raise HTTPException(status_code=404, detail="Task not found in the specified project") return db_task @@ -42,7 +42,7 @@ def get_projects(db: db_dependency, request: Request): user_id = getattr(user, "id") ## fetching projects for the user - projects = db.query(Project).join(Project.users).filter(getattr(UserBase, "id") == int(user_id)).all() + projects = db.query(Project).join(Project.users).filter(User.id == user_id).all() return projects @@ -51,7 +51,8 @@ def get_project(project_id: int, request:Request, db: db_dependency): """Get a project by ID""" user = get_user_from_jwt(request, db) - return get_project_by_id_for_user(user, project_id, db) + project = get_project_by_id_for_user(user, project_id, db) + return project @router.get("/{project_id}/users", response_model=List[UserBase], tags=["users", "projects"]) def get_project_users(project_id: int, request:Request, db: db_dependency): From 4a2675a6f32e7171aea07f20f77d028bdcef0ca0 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 13:30:40 +0100 Subject: [PATCH 12/14] Added AGPL-3.0-or-later license to FastAPI metadata --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 280c2fe..377e1f9 100644 --- a/main.py +++ b/main.py @@ -32,7 +32,10 @@ async def lifespan(app: FastAPI): init_db() yield -app = FastAPI(lifespan=lifespan) +app = FastAPI( + lifespan=lifespan, + license_info={"name": "AGPL-3.0-or-later", "url": "https://www.gnu.org/licenses/agpl-3.0.en.html"} + ) app.add_middleware( CORSMiddleware, From b7d24a85e4ab6d7287055bb392fac14cc031a90f Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 13:35:10 +0100 Subject: [PATCH 13/14] Added basic description to FastAPI docs --- main.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 377e1f9..4d623c9 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,26 @@ from routers.auth import router as auth_router from routers.me import router as me_router from database import init_db +app_description = """ +This API serves as the backend for a Kanban-style project management application. +It allows users to manage projects, tasks, and user assignments with proper authentication and authorization. + +## Stack +- FastAPI +- SQLAlchemy +- SQLite + +## Features +- User Authentication (JWT and Argon2 Password Hashing) +- Project Management (Create, Read, Update, Delete) +- Task Management within Projects +- User Assignments to Projects +- CORS Configuration for Frontend Integration + +## Source Code +The source code for this API can be found on [GitHub](https://github.com/a-mayb3/Kanban_clone_backend) or [my forgejo instance](https://git.vollex.cc/a-mayb3/Kanban_clone_backend). +""" + global_logger = logging.getLogger() global_logger.setLevel(logging.INFO) logging.basicConfig( @@ -34,7 +54,9 @@ async def lifespan(app: FastAPI): app = FastAPI( lifespan=lifespan, - license_info={"name": "AGPL-3.0-or-later", "url": "https://www.gnu.org/licenses/agpl-3.0.en.html"} + license_info={"name": "AGPL-3.0-or-later", "url": "https://www.gnu.org/licenses/agpl-3.0.en.html"}, + description=app_description, + title="Kanban Clone Backend API", ) app.add_middleware( From 7b011fd887690848ffbb5812e7529bf7d46483d6 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 13:37:23 +0100 Subject: [PATCH 14/14] removed TODO comments from main.py --- main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/main.py b/main.py index 4d623c9..ba3776f 100644 --- a/main.py +++ b/main.py @@ -82,10 +82,6 @@ def ping(): def source(): return {"url": "https://github.com/a-mayb3/Kanban_clone_backend"} -## TODO: Add root endpoint that gives basic info about the API -## TODO: Add more detailed error handling and logging -## TODO: Implement authentication and authorization mechanisms - if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)