From f08f7487124dd9839922a29f69c1d07f876b271b Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 14:35:00 +0100 Subject: [PATCH 01/65] added fastapi to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e8a3a45 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +fastapi==0.128.0 From 45f1395303d9d2ebfe7b07b8eaeb9d195ba1daee Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 15:15:56 +0100 Subject: [PATCH 02/65] requirements.txt --- requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/requirements.txt b/requirements.txt index e8a3a45..eaf09c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,10 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.1 fastapi==0.128.0 +idna==3.11 +pydantic==2.12.5 +pydantic_core==2.41.5 +starlette==0.50.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 From d76caf491c0087eb082b8c5aa8a15ba798ec5f25 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 15:37:03 +0100 Subject: [PATCH 03/65] Added SQLAlchemy and psycopg2 to requirements.txt --- requirements.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/requirements.txt b/requirements.txt index eaf09c9..6fc6537 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,45 @@ annotated-doc==0.0.4 annotated-types==0.7.0 anyio==4.12.1 +certifi==2026.1.4 +click==8.3.1 +dnspython==2.8.0 +email-validator==2.3.0 fastapi==0.128.0 +fastapi-cli==0.0.20 +fastapi-cloud-cli==0.11.0 +fastar==0.8.0 +greenlet==3.3.1 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.7.1 +httpx==0.28.1 idna==3.11 +Jinja2==3.1.6 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +psycopg2-binary==2.9.11 pydantic==2.12.5 +pydantic-extra-types==2.11.0 +pydantic-settings==2.12.0 pydantic_core==2.41.5 +Pygments==2.19.2 +python-dotenv==1.2.1 +python-multipart==0.0.22 +PyYAML==6.0.3 +rich==14.3.1 +rich-toolkit==0.17.1 +rignore==0.7.6 +sentry-sdk==2.50.0 +shellingham==1.5.4 +SQLAlchemy==2.0.46 starlette==0.50.0 +typer==0.21.1 typing-inspection==0.4.2 typing_extensions==4.15.0 +urllib3==2.6.3 +uvicorn==0.40.0 +uvloop==0.22.1 +watchfiles==1.1.1 +websockets==16.0 From 8b91f3441f2fc74aff7325e1b34bb6ac30d8b571 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 15:49:47 +0100 Subject: [PATCH 04/65] Created basic classes for tasks, projects, and task statuses --- main.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..5ccc9a7 --- /dev/null +++ b/main.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI, HTTPException, Depends +from pydantic import BaseModel +from typing import List, Annotated + +app = FastAPI() + +class TaskStatus(): + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + FAILED = "failed" + STASHED = "stashed" + +class TaskBase(BaseModel): + id: int + title: str + description: str + status: TaskStatus + +class ProjectBase(BaseModel): + id: int + name: str + description: str + tasks: List[TaskBase] From 19ad5e97cd2586b2575bf6cf14bc6fcf51d420c2 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 16:34:20 +0100 Subject: [PATCH 05/65] Ignoring .db sqlite files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b7faf40..782f859 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.db + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] From 41ca480363a08e7cbbf06ed84841d92dbb8ec08a Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:51:51 +0100 Subject: [PATCH 06/65] Defined tables for sqlite db --- models.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 models.py diff --git a/models.py b/models.py new file mode 100644 index 0000000..72db7b3 --- /dev/null +++ b/models.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, ForeignKey, String, Integer, Table +from sqlalchemy.orm import relationship +from database import Base +from pydantic import BaseModel +from typing import Optional, List + +project_user = Table( + "project_user", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id"), primary_key=True), + Column("user_id", Integer, ForeignKey("users.id"), primary_key=True) +) + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + email = Column(String, unique=True, index=True) + projects = relationship("Project", secondary=project_user, back_populates="users") + +class Project(Base): + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + description = Column(String) + users = relationship("User", secondary=project_user, back_populates="projects") + tasks = relationship("Task", back_populates="project") + +class Task(Base): + __tablename__ = "tasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String) + status = Column(String, default="pending") + project_id = Column(Integer, ForeignKey("projects.id")) + project = relationship("Project", back_populates="tasks") \ No newline at end of file From b3899de769f8749671f5c857d53bb0e73a5a832e Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:52:20 +0100 Subject: [PATCH 07/65] Base pydantic models for task managment --- tasks.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tasks.py diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..f383fd9 --- /dev/null +++ b/tasks.py @@ -0,0 +1,26 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict +from typing import List, Annotated, Optional +import models +from database import SessionLocal, engine +from sqlalchemy.orm import Session, joinedload + +class TaskStatus(str, Enum): + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + FAILED = "failed" + STASHED = "stashed" + +class TaskBase(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + title: str + description: Optional[str] = None + status: TaskStatus = TaskStatus.PENDING + +class TaskList(BaseModel): + model_config = ConfigDict(from_attributes=True) + + tasks: List[TaskBase] From 05eb7e0e5cada635e16b4bd272ec1d289337e153 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:53:13 +0100 Subject: [PATCH 08/65] Base pydantic models for project managment --- projects.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 projects.py diff --git a/projects.py b/projects.py new file mode 100644 index 0000000..bffd59b --- /dev/null +++ b/projects.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Optional + +from tasks import TaskBase +from 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 ProjectCreate(BaseModel): + name: str + description: Optional[str] = None + tasks: List[TaskBase] = [] + user_ids: List[int] = [] + +class ProjectList(BaseModel): + model_config = ConfigDict(from_attributes=True) + + projects: List[ProjectBase] \ No newline at end of file From 552ab862bb5ee7f3f23a0a414d72e5259c433094 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:53:30 +0100 Subject: [PATCH 09/65] Base pydantic models for user managment --- users.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 users.py diff --git a/users.py b/users.py new file mode 100644 index 0000000..9e5f42e --- /dev/null +++ b/users.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, ConfigDict +from typing import List + +class UserBase(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + name: str + email: str + +class UserList(BaseModel): + model_config = ConfigDict(from_attributes=True) + + users: List[UserBase] \ No newline at end of file From ec9e0fad7863495765f556ad270ae82c8ade0347 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:53:50 +0100 Subject: [PATCH 10/65] Starting working on endpoints --- main.py | 78 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/main.py b/main.py index 5ccc9a7..c1cac3f 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,66 @@ +from enum import Enum from fastapi import FastAPI, HTTPException, Depends -from pydantic import BaseModel -from typing import List, Annotated +from pydantic import BaseModel, ConfigDict +from typing import List, Annotated, Optional + +import models +from database import SessionLocal, engine +from sqlalchemy.orm import Session, joinedload + +from tasks import TaskBase, TaskList +from users import UserBase +from projects import ProjectBase, ProjectCreate, ProjectList app = FastAPI() -class TaskStatus(): - PENDING = "pending" - IN_PROGRESS = "in_progress" - COMPLETED = "completed" - FAILED = "failed" - STASHED = "stashed" +models.Base.metadata.create_all(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +db_dependency = Annotated[Session, Depends(get_db)] + +@app.post("/projects/", response_model=ProjectBase) +def create_project(project: ProjectCreate, db: db_dependency): + db_project = models.Project(name=project.name, description=project.description) + db.add(db_project) + db.commit() + db.refresh(db_project) + + for task in project.tasks: + db_task = models.Task( + title=task.title, + description=task.description, + status=task.status.value, + project_id=db_project.id + ) + db.add(db_task) + db.commit() + + if project.user_ids: + users = db.query(models.User).filter(models.User.id.in_(project.user_ids)).all() + db_project.users.extend(users) + db.commit() + + db_project = db.query(models.Project).options( + joinedload(models.Project.tasks), + joinedload(models.Project.users) + ).filter(models.Project.id == db_project.id).first() + + return db_project + +@app.get("/projects/{project_id}", response_model=ProjectBase) +def read_project(project_id: int, db: db_dependency): + db_project = db.query(models.Project).options( + joinedload(models.Project.tasks), + joinedload(models.Project.users) + ).filter(models.Project.id == project_id).first() + if db_project is None: + raise HTTPException(status_code=404, detail="Project not found") + return db_project -class TaskBase(BaseModel): - id: int - title: str - description: str - status: TaskStatus -class ProjectBase(BaseModel): - id: int - name: str - description: str - tasks: List[TaskBase] From 6cd7bf8da2c33acbeb77074f142c10391fc44c4a Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:54:17 +0100 Subject: [PATCH 11/65] Connection to sqlite db file --- database.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 database.py diff --git a/database.py b/database.py new file mode 100644 index 0000000..8a70e4f --- /dev/null +++ b/database.py @@ -0,0 +1,10 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +URL_DATABASE = "sqlite:///./kanban_clone.db" + +engine = create_engine(URL_DATABASE) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() From 170446fcc24d0d0c8491ce1f14071d2495ee7ef6 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 17:57:00 +0100 Subject: [PATCH 12/65] moved pydantic models to misc/ --- main.py | 6 +++--- projects.py => misc/projects.py | 4 ++-- tasks.py => misc/tasks.py | 0 users.py => misc/users.py | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename projects.py => misc/projects.py (89%) rename tasks.py => misc/tasks.py (100%) rename users.py => misc/users.py (100%) diff --git a/main.py b/main.py index c1cac3f..c3974b5 100644 --- a/main.py +++ b/main.py @@ -7,9 +7,9 @@ import models from database import SessionLocal, engine from sqlalchemy.orm import Session, joinedload -from tasks import TaskBase, TaskList -from users import UserBase -from projects import ProjectBase, ProjectCreate, ProjectList +from misc.tasks import TaskBase, TaskList +from misc.users import UserBase +from misc.projects import ProjectBase, ProjectCreate, ProjectList app = FastAPI() diff --git a/projects.py b/misc/projects.py similarity index 89% rename from projects.py rename to misc/projects.py index bffd59b..de07f37 100644 --- a/projects.py +++ b/misc/projects.py @@ -1,8 +1,8 @@ from pydantic import BaseModel, ConfigDict from typing import List, Optional -from tasks import TaskBase -from users import UserBase +from misc.tasks import TaskBase +from misc.users import UserBase class ProjectBase(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/tasks.py b/misc/tasks.py similarity index 100% rename from tasks.py rename to misc/tasks.py diff --git a/users.py b/misc/users.py similarity index 100% rename from users.py rename to misc/users.py From 3d8edb8fa1f58ba16de222be542925d92f175ed0 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 18:05:51 +0100 Subject: [PATCH 13/65] Removed superfluous response models --- misc/projects.py | 5 ----- misc/tasks.py | 4 ---- misc/users.py | 5 ----- 3 files changed, 14 deletions(-) diff --git a/misc/projects.py b/misc/projects.py index de07f37..f499467 100644 --- a/misc/projects.py +++ b/misc/projects.py @@ -18,8 +18,3 @@ class ProjectCreate(BaseModel): description: Optional[str] = None tasks: List[TaskBase] = [] user_ids: List[int] = [] - -class ProjectList(BaseModel): - model_config = ConfigDict(from_attributes=True) - - projects: List[ProjectBase] \ No newline at end of file diff --git a/misc/tasks.py b/misc/tasks.py index f383fd9..63348ce 100644 --- a/misc/tasks.py +++ b/misc/tasks.py @@ -20,7 +20,3 @@ class TaskBase(BaseModel): description: Optional[str] = None status: TaskStatus = TaskStatus.PENDING -class TaskList(BaseModel): - model_config = ConfigDict(from_attributes=True) - - tasks: List[TaskBase] diff --git a/misc/users.py b/misc/users.py index 9e5f42e..452c328 100644 --- a/misc/users.py +++ b/misc/users.py @@ -7,8 +7,3 @@ class UserBase(BaseModel): id: int name: str email: str - -class UserList(BaseModel): - model_config = ConfigDict(from_attributes=True) - - users: List[UserBase] \ No newline at end of file From 2c92627949dc8827c6d772e170e92db0529345d5 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 18:20:31 +0100 Subject: [PATCH 14/65] Removed inexistent imports --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index c3974b5..e7b2927 100644 --- a/main.py +++ b/main.py @@ -7,9 +7,9 @@ import models from database import SessionLocal, engine from sqlalchemy.orm import Session, joinedload -from misc.tasks import TaskBase, TaskList +from misc.tasks import TaskBase from misc.users import UserBase -from misc.projects import ProjectBase, ProjectCreate, ProjectList +from misc.projects import ProjectBase, ProjectCreate app = FastAPI() From 88180c9bd7481a873ccc211f663720101c7020d0 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 26 Jan 2026 18:22:34 +0100 Subject: [PATCH 15/65] updated requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6fc6537..ddec978 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,6 @@ Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 -psycopg2-binary==2.9.11 pydantic==2.12.5 pydantic-extra-types==2.11.0 pydantic-settings==2.12.0 From 0b676688c2227638c45173c0f9e0ab3d024b88c7 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 11:08:10 +0100 Subject: [PATCH 16/65] renamed misc/ to schemas/ --- {misc => schemas}/projects.py | 0 {misc => schemas}/tasks.py | 4 ---- {misc => schemas}/users.py | 0 3 files changed, 4 deletions(-) rename {misc => schemas}/projects.py (100%) rename {misc => schemas}/tasks.py (81%) rename {misc => schemas}/users.py (100%) diff --git a/misc/projects.py b/schemas/projects.py similarity index 100% rename from misc/projects.py rename to schemas/projects.py diff --git a/misc/tasks.py b/schemas/tasks.py similarity index 81% rename from misc/tasks.py rename to schemas/tasks.py index 63348ce..f089473 100644 --- a/misc/tasks.py +++ b/schemas/tasks.py @@ -1,9 +1,6 @@ from enum import Enum from pydantic import BaseModel, ConfigDict from typing import List, Annotated, Optional -import models -from database import SessionLocal, engine -from sqlalchemy.orm import Session, joinedload class TaskStatus(str, Enum): PENDING = "pending" @@ -19,4 +16,3 @@ class TaskBase(BaseModel): title: str description: Optional[str] = None status: TaskStatus = TaskStatus.PENDING - diff --git a/misc/users.py b/schemas/users.py similarity index 100% rename from misc/users.py rename to schemas/users.py From abd4d5e988c6cc4a0edecf08c8b92d212a8b67a7 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 11:15:00 +0100 Subject: [PATCH 17/65] Started working on projects related endpoints --- database.py | 19 ++++++++++++++ main.py | 64 +++------------------------------------------ models.py | 1 - routers/projects.py | 52 ++++++++++++++++++++++++++++++++++++ routers/tasks.py | 16 ++++++++++++ schemas/projects.py | 4 +-- 6 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 routers/projects.py create mode 100644 routers/tasks.py diff --git a/database.py b/database.py index 8a70e4f..0d630a9 100644 --- a/database.py +++ b/database.py @@ -2,9 +2,28 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base +from pydantic import BaseModel, ConfigDict + +from fastapi import Depends +from sqlalchemy.orm import Session +from typing import Annotated + + + URL_DATABASE = "sqlite:///./kanban_clone.db" engine = create_engine(URL_DATABASE) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() + +Base.metadata.create_all(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +db_dependency = Annotated[Session, Depends(get_db)] \ No newline at end of file diff --git a/main.py b/main.py index e7b2927..bdc931b 100644 --- a/main.py +++ b/main.py @@ -1,66 +1,10 @@ -from enum import Enum from fastapi import FastAPI, HTTPException, Depends -from pydantic import BaseModel, ConfigDict -from typing import List, Annotated, Optional -import models -from database import SessionLocal, engine -from sqlalchemy.orm import Session, joinedload - -from misc.tasks import TaskBase -from misc.users import UserBase -from misc.projects import ProjectBase, ProjectCreate +from routers.tasks import router as tasks_router +from routers.projects import router as projects_router app = FastAPI() -models.Base.metadata.create_all(bind=engine) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -db_dependency = Annotated[Session, Depends(get_db)] - -@app.post("/projects/", response_model=ProjectBase) -def create_project(project: ProjectCreate, db: db_dependency): - db_project = models.Project(name=project.name, description=project.description) - db.add(db_project) - db.commit() - db.refresh(db_project) - - for task in project.tasks: - db_task = models.Task( - title=task.title, - description=task.description, - status=task.status.value, - project_id=db_project.id - ) - db.add(db_task) - db.commit() - - if project.user_ids: - users = db.query(models.User).filter(models.User.id.in_(project.user_ids)).all() - db_project.users.extend(users) - db.commit() - - db_project = db.query(models.Project).options( - joinedload(models.Project.tasks), - joinedload(models.Project.users) - ).filter(models.Project.id == db_project.id).first() - - return db_project - -@app.get("/projects/{project_id}", response_model=ProjectBase) -def read_project(project_id: int, db: db_dependency): - db_project = db.query(models.Project).options( - joinedload(models.Project.tasks), - joinedload(models.Project.users) - ).filter(models.Project.id == project_id).first() - if db_project is None: - raise HTTPException(status_code=404, detail="Project not found") - return db_project - +app.include_router(tasks_router) +app.include_router(projects_router) diff --git a/models.py b/models.py index 72db7b3..baa926f 100644 --- a/models.py +++ b/models.py @@ -1,7 +1,6 @@ from sqlalchemy import Column, ForeignKey, String, Integer, Table from sqlalchemy.orm import relationship from database import Base -from pydantic import BaseModel from typing import Optional, List project_user = Table( diff --git a/routers/projects.py b/routers/projects.py new file mode 100644 index 0000000..38072f5 --- /dev/null +++ b/routers/projects.py @@ -0,0 +1,52 @@ +from fastapi import APIRouter, HTTPException, Depends +from typing import List, Annotated + +from database import db_dependency + +import schemas.tasks as tasks +import schemas.projects as projects + +router = APIRouter(prefix="/projects", tags=["projects"]) + +@router.get("/{project_id}", response_model=projects.ProjectBase) +def read_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 + +@router.get("/{project_id}/tasks", response_model=List[tasks.TaskBase]) +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.TaskBase) +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 + +@router.post("/", response_model=projects.ProjectCreate) +def create_project(project: projects.ProjectCreate, db: db_dependency): + db_project = projects.models.Project( + name=project.name, + description=project.description + ) + db.add(db_project) + db.commit() + db.refresh(db_project) + return db_project + +@router.post("/{project_id}/tasks", response_model=tasks.TaskBase) +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 \ No newline at end of file diff --git a/routers/tasks.py b/routers/tasks.py new file mode 100644 index 0000000..c17085e --- /dev/null +++ b/routers/tasks.py @@ -0,0 +1,16 @@ +from typing import List +from schemas.tasks import TaskBase +from fastapi import APIRouter, HTTPException, Depends + +from database import db_dependency + +import models + +router = APIRouter(prefix="/tasks", tags=["tasks"]) + +# """Get tasks from a specified project""" +# @router.get("/from_project/{project_id}", response_model=List[TaskBase]) +# def read_tasks_from_project(project_id: int, db: db_dependency): +# db_tasks = db.query(models.Task).filter(models.Task.project_id == project_id).all() +# return db_tasks + diff --git a/schemas/projects.py b/schemas/projects.py index f499467..243d2a4 100644 --- a/schemas/projects.py +++ b/schemas/projects.py @@ -1,8 +1,8 @@ from pydantic import BaseModel, ConfigDict from typing import List, Optional -from misc.tasks import TaskBase -from misc.users import UserBase +from schemas.tasks import TaskBase +from schemas.users import UserBase class ProjectBase(BaseModel): model_config = ConfigDict(from_attributes=True) From d76a50f5f0b55deb3b9250206cf82d8f42b5f3ff Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 11:16:17 +0100 Subject: [PATCH 18/65] ping pong :) --- main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main.py b/main.py index bdc931b..1c25965 100644 --- a/main.py +++ b/main.py @@ -8,3 +8,7 @@ app = FastAPI() app.include_router(tasks_router) app.include_router(projects_router) +"""ping pong :)""" +@app.get("/ping") +def ping(): + return {"message": "pong"} \ No newline at end of file From e3be0498704e2eef04d6b5f0cfc0f2960c352a2f Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:46:45 +0100 Subject: [PATCH 19/65] Added some joke and info root endpoint and some TODOs --- main.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 1c25965..ba94a7a 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,23 @@ from fastapi import FastAPI, HTTPException, Depends -from routers.tasks import router as tasks_router from routers.projects import router as projects_router +from routers.users import router as users_router app = FastAPI() -app.include_router(tasks_router) +app.include_router(users_router) app.include_router(projects_router) """ping pong :)""" @app.get("/ping") def ping(): - return {"message": "pong"} \ No newline at end of file + return {"message": "pong"} + +"""Gives project url""" +@app.get("/sources") +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 From 57a6ec2676422955f17e9b1038e991276e275d0f Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:47:39 +0100 Subject: [PATCH 20/65] Removed superfluous tasks routers since tasks depend on projects --- routers/tasks.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 routers/tasks.py diff --git a/routers/tasks.py b/routers/tasks.py deleted file mode 100644 index c17085e..0000000 --- a/routers/tasks.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import List -from schemas.tasks import TaskBase -from fastapi import APIRouter, HTTPException, Depends - -from database import db_dependency - -import models - -router = APIRouter(prefix="/tasks", tags=["tasks"]) - -# """Get tasks from a specified project""" -# @router.get("/from_project/{project_id}", response_model=List[TaskBase]) -# def read_tasks_from_project(project_id: int, db: db_dependency): -# db_tasks = db.query(models.Task).filter(models.Task.project_id == project_id).all() -# return db_tasks - From d689fbc45354a5bf57e34121481e1b18d3a1cac5 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:48:10 +0100 Subject: [PATCH 21/65] Added password_hash to user table --- models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models.py b/models.py index baa926f..b1f5390 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, ForeignKey, String, Integer, Table from sqlalchemy.orm import relationship +from sqlalchemy.dialects.sqlite import BLOB from database import Base from typing import Optional, List @@ -16,6 +17,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) email = Column(String, unique=True, index=True) + password_hash = Column(BLOB) projects = relationship("Project", secondary=project_user, back_populates="users") class Project(Base): From 73d54b6fe76ea2180d290e06ea6da218c9ef5d56 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:55:56 +0100 Subject: [PATCH 22/65] Added basic user operations --- routers/users.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 routers/users.py diff --git a/routers/users.py b/routers/users.py new file mode 100644 index 0000000..f746b38 --- /dev/null +++ b/routers/users.py @@ -0,0 +1,63 @@ +from typing import List +from fastapi import APIRouter, HTTPException, Depends +from database import db_dependency + +import models + +import schemas.users as users +import schemas.projects as projects + +router = APIRouter(prefix="/users", tags=["users"]) + + + +"""Get a user by ID""" + +@router.get("/{user_id}", response_model=users.UserBase) +def read_user(user_id: int, db: db_dependency): + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +"""Update a user by ID""" + +@router.put("/{user_id}", response_model=users.UserBase) +def update_user(user_id: int, user: users.UserBase, db: db_dependency): + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + db_user.name = user.name + db_user.email = user.email + db.commit() + db.refresh(db_user) + return db_user + + +"""Get projects assigned to a user""" + +@router.get("/{user_id}/projects", response_model=List[projects.ProjectBase]) +def read_projects_from_user(user_id: int, db: db_dependency): + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user.projects + +## +## POST endpoints +## + + +"""Create a new user""" + +@router.post("/", response_model=users.UserBase) +def create_user(user: users.UserBase, db: db_dependency): + db_user = models.User( + name=user.name, + email=user.email + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user \ No newline at end of file From df70a954185dca8049c4280071a50ae173a33296 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:57:26 +0100 Subject: [PATCH 23/65] Added schemas for user creation and modification --- schemas/tasks.py | 6 ++++++ schemas/users.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/schemas/tasks.py b/schemas/tasks.py index f089473..ea3b711 100644 --- a/schemas/tasks.py +++ b/schemas/tasks.py @@ -16,3 +16,9 @@ class TaskBase(BaseModel): title: str description: Optional[str] = None status: TaskStatus = TaskStatus.PENDING + +class TaskUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TaskStatus] = None + diff --git a/schemas/users.py b/schemas/users.py index 452c328..30bf93f 100644 --- a/schemas/users.py +++ b/schemas/users.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, ConfigDict -from typing import List +from typing import List, Optional class UserBase(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -7,3 +7,15 @@ class UserBase(BaseModel): id: int name: str email: str + +class UserCreate(UserBase): + password: str + +class UserUpdateInfo(BaseModel): + name: Optional[str] = None + email: Optional[str] = None + +class UserUpdatePassword(BaseModel): + password: str + new_password: str + From 4e4f25de069435ab2dd1cd6be64d3e4d19865bde Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:58:14 +0100 Subject: [PATCH 24/65] Added some project schemas for project managment --- schemas/projects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/schemas/projects.py b/schemas/projects.py index 243d2a4..6522337 100644 --- a/schemas/projects.py +++ b/schemas/projects.py @@ -18,3 +18,13 @@ class ProjectCreate(BaseModel): description: Optional[str] = None tasks: List[TaskBase] = [] user_ids: List[int] = [] + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + +class ProjectAddUsers(BaseModel): + user_ids: List[int] = [] + +class ProjectRemoveUsers(BaseModel): + user_ids: List[int] = [] From 68bdba08fe6e7ed654ff742ad9a57b0a856e36ac Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 27 Jan 2026 12:58:50 +0100 Subject: [PATCH 25/65] Created a bunch of endpoints but forgetting to commit accordingly :) --- routers/projects.py | 137 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 38072f5..01a0d0f 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -5,9 +5,17 @@ from database import db_dependency import schemas.tasks as tasks import schemas.projects as projects +import schemas.users as users router = APIRouter(prefix="/projects", tags=["projects"]) +## +## GET endpoints +## + + +"""Get a project by ID""" + @router.get("/{project_id}", response_model=projects.ProjectBase) def read_project(project_id: int, db: db_dependency): db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() @@ -15,18 +23,38 @@ def read_project(project_id: int, db: db_dependency): raise HTTPException(status_code=404, detail="Project not found") return db_project -@router.get("/{project_id}/tasks", response_model=List[tasks.TaskBase]) +"""Get tasks from a specified project""" +@router.get("/{project_id}/tasks", response_model=List[tasks.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.TaskBase) +"""Get a specific task from a specified project""" +@router.get("/{project_id}/tasks/{task_id}", response_model=tasks.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 + +"""Get users from a specified project""" + +@router.get("/{project_id}/users", response_model=List[users.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 + + +## +## POST endpoints +## + + +"""Create a new project""" + @router.post("/", response_model=projects.ProjectCreate) def create_project(project: projects.ProjectCreate, db: db_dependency): db_project = projects.models.Project( @@ -38,7 +66,10 @@ def create_project(project: projects.ProjectCreate, db: db_dependency): db.refresh(db_project) return db_project -@router.post("/{project_id}/tasks", response_model=tasks.TaskBase) + +"""Create a new task in a specified 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, @@ -49,4 +80,102 @@ def create_task_in_project(project_id: int, task: tasks.TaskBase, db: db_depende db.add(db_task) db.commit() db.refresh(db_task) - return db_task \ No newline at end of file + return db_task + + +"""Add users to a specified project using their IDs""" + +@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 + + +## +## PUT endpoints +## + + +"""Update a project by ID""" + +@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 + + +"""Update a task in a specified project""" + +@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 + +## +## DELETE endpoints +## + +"""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"} From 8b5f35ddad3879fbe0024ebb559a6792ecd4d276 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 08:17:56 +0100 Subject: [PATCH 26/65] Added pyargon2 to dependencies --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index ddec978..a9c3721 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ annotated-doc==0.0.4 annotated-types==0.7.0 anyio==4.12.1 certifi==2026.1.4 +cffi==2.0.0 click==8.3.1 dnspython==2.8.0 email-validator==2.3.0 @@ -19,6 +20,8 @@ Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +pyargon2==1.1.2 +pycparser==3.0 pydantic==2.12.5 pydantic-extra-types==2.11.0 pydantic-settings==2.12.0 From 6c8729800393b0642722de47f627edb1a02b3077 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:05:11 +0100 Subject: [PATCH 27/65] added crypt and auth dependencies --- requirements.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/requirements.txt b/requirements.txt index a9c3721..cc52ab5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,13 @@ annotated-doc==0.0.4 annotated-types==0.7.0 anyio==4.12.1 +bcrypt==5.0.0 certifi==2026.1.4 cffi==2.0.0 click==8.3.1 +cryptography==46.0.4 dnspython==2.8.0 +ecdsa==0.19.1 email-validator==2.3.0 fastapi==0.128.0 fastapi-cli==0.0.20 @@ -16,11 +19,14 @@ httpcore==1.0.9 httptools==0.7.1 httpx==0.28.1 idna==3.11 +itsdangerous==2.2.0 Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +passlib==1.7.4 pyargon2==1.1.2 +pyasn1==0.6.2 pycparser==3.0 pydantic==2.12.5 pydantic-extra-types==2.11.0 @@ -28,15 +34,20 @@ pydantic-settings==2.12.0 pydantic_core==2.41.5 Pygments==2.19.2 python-dotenv==1.2.1 +python-jose==3.5.0 python-multipart==0.0.22 PyYAML==6.0.3 rich==14.3.1 rich-toolkit==0.17.1 rignore==0.7.6 +rsa==4.9.1 sentry-sdk==2.50.0 shellingham==1.5.4 +six==1.17.0 SQLAlchemy==2.0.46 starlette==0.50.0 +starlette-session==0.4.3 +starlette-session-middleware==0.1.6 typer==0.21.1 typing-inspection==0.4.2 typing_extensions==4.15.0 From e557c257895207092a6311263ce1ed997446ee0c Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:06:42 +0100 Subject: [PATCH 28/65] Added more .gitignore entries --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 782f859..3f840d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ *.db +.idea/ +.vscode/ + +.env* + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] From b909a23fa328c1b49a2ba4941ef23e53845b6bd2 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:09:27 +0100 Subject: [PATCH 29/65] Started implementing auth --- database.py | 11 +++-- main.py | 63 +++++++++++++++++++++++- models.py | 11 +++-- routers/auth.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++ routers/me.py | 45 +++++++++++++++++ routers/users.py | 35 ++++++++++--- schemas/users.py | 7 ++- 7 files changed, 279 insertions(+), 19 deletions(-) create mode 100644 routers/auth.py create mode 100644 routers/me.py diff --git a/database.py b/database.py index 0d630a9..3c253b3 100644 --- a/database.py +++ b/database.py @@ -1,3 +1,5 @@ + +import sqlalchemy from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base @@ -8,16 +10,17 @@ from fastapi import Depends from sqlalchemy.orm import Session from typing import Annotated - - URL_DATABASE = "sqlite:///./kanban_clone.db" -engine = create_engine(URL_DATABASE) +engine = create_engine(URL_DATABASE, echo=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() -Base.metadata.create_all(bind=engine) +def init_db() -> None: + # Import models so they are registered with SQLAlchemy metadata + import models # noqa: F401 + Base.metadata.create_all(bind=engine) def get_db(): db = SessionLocal() diff --git a/main.py b/main.py index ba94a7a..8c80a91 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,33 @@ -from fastapi import FastAPI, HTTPException, Depends +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware from routers.projects import router as projects_router + from routers.users import router as users_router +from routers.auth import router as auth_router +from routers.me import router as me_router +from database import init_db -app = FastAPI() +@asynccontextmanager +async def lifespan(app: FastAPI): + # Place for startup and shutdown events if needed in the future + init_db() + yield +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth_router) app.include_router(users_router) +app.include_router(me_router) app.include_router(projects_router) """ping pong :)""" @@ -21,3 +43,40 @@ def source(): ## 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) + + +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +@app.exception_handler(HTTPException) +async def http_exception_handler(request, exc): + """Custom HTTP exception handler""" + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "message": exc.detail, + "type": "authentication_error" if exc.status_code == 401 else "authorization_error", + "status_code": exc.status_code + } + }, + headers=exc.headers + ) + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request, exc): + """Handle validation errors""" + return JSONResponse( + status_code=422, + content={ + "error": { + "message": "Validation error", + "type": "validation_error", + "details": exc.errors() + } + } + ) \ No newline at end of file diff --git a/models.py b/models.py index b1f5390..74562b0 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,6 @@ from sqlalchemy import Column, ForeignKey, String, Integer, Table -from sqlalchemy.orm import relationship from sqlalchemy.dialects.sqlite import BLOB +from sqlalchemy.orm import relationship from database import Base from typing import Optional, List @@ -14,16 +14,17 @@ project_user = Table( class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True, index=True, autoincrement=True) name = Column(String, index=True) email = Column(String, unique=True, index=True) - password_hash = Column(BLOB) + password_hash = Column(String) + password_salt = Column(String) projects = relationship("Project", secondary=project_user, back_populates="users") class Project(Base): __tablename__ = "projects" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True, index=True, autoincrement=True) name = Column(String, index=True) description = Column(String) users = relationship("User", secondary=project_user, back_populates="projects") @@ -32,7 +33,7 @@ class Project(Base): class Task(Base): __tablename__ = "tasks" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True, index=True, autoincrement=True) title = Column(String, index=True) description = Column(String) status = Column(String, default="pending") diff --git a/routers/auth.py b/routers/auth.py new file mode 100644 index 0000000..0b20e0a --- /dev/null +++ b/routers/auth.py @@ -0,0 +1,126 @@ +import os + +from fastapi import APIRouter, Depends, HTTPException, status, Response +from database import db_dependency +from jose import JWTError, jwt +from datetime import datetime, timedelta, timezone +import models + +import schemas.users as user_schemas +import routers.users as user_router + +from pyargon2 import hash + +router = APIRouter(prefix="/auth", tags=["auth"]) + +SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + to_encode.update({"iat": datetime.now(timezone.utc)}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +@router.post("/login") +def login(user_data: user_schemas.UserLogin, response: Response, db: db_dependency): + """Login and receive JWT token in cookie""" + db_user = db.query(models.User).filter(models.User.email == user_data.email).first() + if db_user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + 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" + ) + + 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 + + ) + + # Set JWT in httpOnly cookie + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, + max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60, + samesite="lax", + secure=False # Set to True in production with HTTPS + ) + + return { + "message": "Login successful", + "user": { + "id": db_user.id, + "name": db_user.name, + "email": db_user.email + } + } + +@router.post("/logout") +def logout(response: Response): + """Logout by clearing the JWT cookie""" + response.delete_cookie(key="access_token") + return {"message": "Logout successful"} + +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 get_current_user(request, db: db_dependency): + """Get current authenticated user from cookie""" + token = request.cookies.get("access_token") + + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated" + ) + + user_id = verify_jwt_token(token) + user = db.query(models.User).filter(models.User.id == int(user_id)).first() + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found" + ) + + return user + + +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 diff --git a/routers/me.py b/routers/me.py new file mode 100644 index 0000000..256ef81 --- /dev/null +++ b/routers/me.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Response, Request +from database import db_dependency +from jose import JWTError, jwt +from datetime import datetime, timedelta, timezone +import models +import os + +from routers import auth +import schemas.users as user_schemas +import routers.users as user_router + +router = APIRouter(prefix="/me", tags=["me"]) + +@router.get("/", response_model=user_schemas.UserBase) +def get_me(request: Request, db: db_dependency): + """Get current authenticated user""" + token = request.cookies.get("access_token") + + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not logged in" + ) + + try: + payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not logged in" + ) + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials" + ) + + db_user = db.query(models.User).filter(models.User.id == int(user_id)).first() + if db_user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found" + ) + return db_user \ No newline at end of file diff --git a/routers/users.py b/routers/users.py index f746b38..215dd15 100644 --- a/routers/users.py +++ b/routers/users.py @@ -1,3 +1,4 @@ +import os from typing import List from fastapi import APIRouter, HTTPException, Depends from database import db_dependency @@ -7,10 +8,11 @@ import models import schemas.users as users import schemas.projects as projects +from pyargon2 import hash +import pyargon2 + router = APIRouter(prefix="/users", tags=["users"]) - - """Get a user by ID""" @router.get("/{user_id}", response_model=users.UserBase) @@ -28,8 +30,8 @@ def update_user(user_id: int, user: users.UserBase, db: db_dependency): db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") - db_user.name = user.name - db_user.email = user.email + setattr(db_user, "name", user.name) + setattr(db_user, "email", user.email) db.commit() db.refresh(db_user) return db_user @@ -52,12 +54,31 @@ def read_projects_from_user(user_id: int, db: db_dependency): """Create a new user""" @router.post("/", response_model=users.UserBase) -def create_user(user: users.UserBase, db: db_dependency): +def create_user(user: users.UserCreate, db: db_dependency): + + user_salt = os.urandom(32).hex() + print("Generated salt:", user_salt) + + hashed_password = hash(password=user.password, salt=user_salt, variant="id") + db_user = models.User( name=user.name, - email=user.email + email=user.email, + password_hash=hashed_password, + password_salt=user_salt ) + db.add(db_user) db.commit() db.refresh(db_user) - return db_user \ No newline at end of file + return db_user + +@router.delete("/{user_id}") +def delete_user(user_id: int, db: db_dependency): + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + db.delete(db_user) + db.commit() + return {"detail": "User deleted"} + diff --git a/schemas/users.py b/schemas/users.py index 30bf93f..1d663da 100644 --- a/schemas/users.py +++ b/schemas/users.py @@ -8,7 +8,9 @@ class UserBase(BaseModel): name: str email: str -class UserCreate(UserBase): +class UserCreate(BaseModel): + name: str + email: str password: str class UserUpdateInfo(BaseModel): @@ -19,3 +21,6 @@ class UserUpdatePassword(BaseModel): password: str new_password: str +class UserLogin(BaseModel): + email: str + password: str From 59ec938c772588dd57b2016d810c978e6487697c Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:22:47 +0100 Subject: [PATCH 30/65] added check for already logged in users --- routers/auth.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/routers/auth.py b/routers/auth.py index 0b20e0a..7b41b8d 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -1,6 +1,6 @@ import os -from fastapi import APIRouter, Depends, HTTPException, status, Response +from fastapi import APIRouter, Depends, HTTPException, Request, status, Response from database import db_dependency from jose import JWTError, jwt from datetime import datetime, timedelta, timezone @@ -30,8 +30,23 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): @router.post("/login") -def login(user_data: user_schemas.UserLogin, response: Response, db: db_dependency): +def login(user_data: user_schemas.UserLogin, request: Request, response: Response, db: db_dependency): """Login and receive JWT token in cookie""" + + ## check if access token already exists + get_token = request.cookies.get("access_token") + if get_token: + try: + user_id = verify_jwt_token(get_token) + return { + "message": "Already logged in", + "user": { + "id": user_id + } + } + except HTTPException: + pass # Token invalid or expired, proceed to login + db_user = db.query(models.User).filter(models.User.email == user_data.email).first() if db_user is None: raise HTTPException( @@ -91,28 +106,6 @@ def verify_jwt_token(token: str): except JWTError: raise credentials_exception -def get_current_user(request, db: db_dependency): - """Get current authenticated user from cookie""" - token = request.cookies.get("access_token") - - if not token: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated" - ) - - user_id = verify_jwt_token(token) - user = db.query(models.User).filter(models.User.id == int(user_id)).first() - - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) - - return user - - 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() From 33c5c008aaf36a7776ac708aabcda8050794fea2 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:23:09 +0100 Subject: [PATCH 31/65] started implementing logger --- main.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 8c80a91..b91885a 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException @@ -9,9 +10,19 @@ from routers.auth import router as auth_router from routers.me import router as me_router from database import init_db +global_logger = logging.getLogger() +global_logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +global_logger.addHandler(handler) + @asynccontextmanager async def lifespan(app: FastAPI): + logger = global_logger + # Place for startup and shutdown events if needed in the future + logger.info("Initializing database...") init_db() yield @@ -55,6 +66,10 @@ from fastapi.responses import JSONResponse @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): """Custom HTTP exception handler""" + + logger = global_logger + logger.error(f"HTTP error occurred: {exc.detail}") + return JSONResponse( status_code=exc.status_code, content={ @@ -69,7 +84,11 @@ async def http_exception_handler(request, exc): @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): - """Handle validation errors""" + """Handle validation errors""" + + logger = global_logger + logger.error(f"Validation error: {exc.errors()}") + return JSONResponse( status_code=422, content={ From c7ab0f00c2aee971681ca6dd5aea79fe4f5117bb Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:27:54 +0100 Subject: [PATCH 32/65] ignoring app.log --- .gitignore | 1 + main.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 3f840d9..784b2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.db +log.app .idea/ .vscode/ diff --git a/main.py b/main.py index b91885a..d19cedd 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,12 @@ from database import init_db global_logger = logging.getLogger() global_logger.setLevel(logging.INFO) +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + filename='app.log', + encoding='utf-8' + ) handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) From 6285ebbd16040eb2cd85942351e9ed13bfc63ce0 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 11:33:46 +0100 Subject: [PATCH 33/65] Added general exception handler --- main.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index d19cedd..280c2fe 100644 --- a/main.py +++ b/main.py @@ -104,4 +104,22 @@ async def validation_exception_handler(request, exc): "details": exc.errors() } } - ) \ No newline at end of file + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request, exc): + """Handle all other exceptions""" + + logger = global_logger + logger.error(f"Unexpected error: {exc}") + + return JSONResponse( + status_code=500, + content={ + "error": { + "message": "An unexpected error occurred.", + "type": "internal_server_error", + "details": str(exc) + } + } + ) \ No newline at end of file From 192b5f9fc5f56b8d46d1225c512d94aa00334fb0 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 13:02:59 +0100 Subject: [PATCH 34/65] transfered logout and added user-deletion --- routers/auth.py | 5 ----- routers/me.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/routers/auth.py b/routers/auth.py index 7b41b8d..6c28901 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -85,11 +85,6 @@ def login(user_data: user_schemas.UserLogin, request: Request, response: Respons } } -@router.post("/logout") -def logout(response: Response): - """Logout by clearing the JWT cookie""" - response.delete_cookie(key="access_token") - return {"message": "Logout successful"} def verify_jwt_token(token: str): """Verify and decode a JWT token""" diff --git a/routers/me.py b/routers/me.py index 256ef81..32dfbea 100644 --- a/routers/me.py +++ b/routers/me.py @@ -42,4 +42,60 @@ def get_me(request: Request, db: db_dependency): status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" ) - return db_user \ No newline at end of file + return db_user + + +@router.post("/logout") +def logout(request: Request,response: Response): + """Logout by clearing the JWT cookie""" + + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not logged in" + ) + + response.delete_cookie(key="access_token") + return {"message": "Logout successful"} + +@router.delete("/delete-me") +def delete_me(request: Request, db: db_dependency): + """Delete current authenticated user""" + token = request.cookies.get("access_token") + + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not logged in" + ) + + try: + payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not logged in" + ) + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials" + ) + +## User retrieval and deletion + user = db.query(models.User).filter(models.User.id == int(user_id)).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found" + ) + + db.delete(user) + db.commit() + + ## Logout user by clearing cookie + request.cookies.clear() + + return {"message": "User deleted successfully"} \ No newline at end of file From f9631cfe875887319b5544d9ba17dc7692f8fb26 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 13:08:37 +0100 Subject: [PATCH 35/65] Added cookie removal on invalid cookies --- routers/me.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/routers/me.py b/routers/me.py index 32dfbea..06c7e57 100644 --- a/routers/me.py +++ b/routers/me.py @@ -1,13 +1,10 @@ from fastapi import APIRouter, Depends, HTTPException, status, Response, Request from database import db_dependency from jose import JWTError, jwt -from datetime import datetime, timedelta, timezone import models -import os from routers import auth import schemas.users as user_schemas -import routers.users as user_router router = APIRouter(prefix="/me", tags=["me"]) @@ -59,7 +56,7 @@ def logout(request: Request,response: Response): response.delete_cookie(key="access_token") return {"message": "Logout successful"} -@router.delete("/delete-me") +@router.delete("/delete-me", tags=["me", "auth", "users"]) def delete_me(request: Request, db: db_dependency): """Delete current authenticated user""" token = request.cookies.get("access_token") @@ -74,6 +71,7 @@ def delete_me(request: Request, db: db_dependency): payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) user_id: str = str(payload.get("sub")) if user_id is None: + request.cookies.clear() ## removing invalid auth cookie raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not logged in" @@ -87,6 +85,7 @@ def delete_me(request: Request, db: db_dependency): ## User retrieval and deletion user = db.query(models.User).filter(models.User.id == int(user_id)).first() if user is None: + request.cookies.clear() ## removing invalid auth cookie raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" @@ -98,4 +97,4 @@ def delete_me(request: Request, db: db_dependency): ## Logout user by clearing cookie request.cookies.clear() - return {"message": "User deleted successfully"} \ No newline at end of file + return {"message": "User deleted successfully"} From 407b22eaf5c7d273f8325383578ce5e7abb5f6a0 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 13:39:22 +0100 Subject: [PATCH 36/65] Started project managment endpoints --- models.py | 2 +- routers/auth.py | 1 - routers/me.py | 6 +- routers/projects.py | 375 ++++++++++++++++++++++++++++---------------- 4 files changed, 243 insertions(+), 141 deletions(-) diff --git a/models.py b/models.py index 74562b0..5b2b8a4 100644 --- a/models.py +++ b/models.py @@ -38,4 +38,4 @@ class Task(Base): description = Column(String) status = Column(String, default="pending") project_id = Column(Integer, ForeignKey("projects.id")) - project = relationship("Project", back_populates="tasks") \ No newline at end of file + project_user = relationship("Project", back_populates="tasks") \ No newline at end of file diff --git a/routers/auth.py b/routers/auth.py index 6c28901..d2d7190 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta, timezone import models import schemas.users as user_schemas -import routers.users as user_router from pyargon2 import hash diff --git a/routers/me.py b/routers/me.py index 06c7e57..1ab8509 100644 --- a/routers/me.py +++ b/routers/me.py @@ -23,11 +23,13 @@ def get_me(request: Request, db: db_dependency): payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) user_id: str = str(payload.get("sub")) if user_id is None: + request.cookies.clear() ## removing invalid auth cookie raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not logged in" ) except JWTError: + request.cookies.clear() ## removing invalid auth cookie raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" @@ -35,6 +37,7 @@ def get_me(request: Request, db: db_dependency): 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=status.HTTP_401_UNAUTHORIZED, detail="User not found" @@ -42,7 +45,7 @@ def get_me(request: Request, db: db_dependency): return db_user -@router.post("/logout") +@router.get("/logout", tags=["me", "auth"]) def logout(request: Request,response: Response): """Logout by clearing the JWT cookie""" @@ -96,5 +99,4 @@ def delete_me(request: Request, db: db_dependency): ## Logout user by clearing cookie request.cookies.clear() - return {"message": "User deleted successfully"} diff --git a/routers/projects.py b/routers/projects.py index 01a0d0f..dff9c62 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -1,11 +1,15 @@ -from fastapi import APIRouter, HTTPException, Depends +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 -import schemas.projects as projects -import schemas.users as users +import schemas.tasks as tasks_schemas +import schemas.projects as projects_schemas +import schemas.users as users_schemas router = APIRouter(prefix="/projects", tags=["projects"]) @@ -13,169 +17,266 @@ 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""" -"""Get a project by ID""" + ## User retrieval from JWT token in cookies + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + + try: + payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + request.cookies.clear() ## User in cookies not found, clear cookies + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + except JWTError: + request.cookies.clear() ## Probably an invalid token, clear cookies + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) -@router.get("/{project_id}", response_model=projects.ProjectBase) -def read_project(project_id: int, db: db_dependency): - db_project = db.query(projects.models.Project).filter(projects.models.Project.id == project_id).first() + ## fetching projects for the user + projects: projects_schemas.ProjectBase | None = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "users.id") == int(user_id)).first() + if projects is None: + return [] + return projects + + +@router.get("/{project_id}", response_model=projects_schemas.ProjectBase) +def get_project(project_id: int, request:Request, db: db_dependency): + """Get a project by ID""" + + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + + try: + payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + request.cookies.clear() ## User in cookies not found, clear cookies + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + except JWTError: + request.cookies.clear() ## Probably an invalid token, clear cookies + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) + user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + + 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 -"""Get tasks from a specified project""" -@router.get("/{project_id}/tasks", response_model=List[tasks.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}/users", response_model=List[users_schemas.UserBase], tags=["users", "projects"]) +def get_project_users(project_id: int, request:Request, db: db_dependency): + """Get users from a specified project""" + + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + + try: + payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + request.cookies.clear() ## User in cookies not found, clear cookies + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + except JWTError: + request.cookies.clear() ## Probably an invalid token, clear cookies + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) + user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() -"""Get a specific task from a specified project""" -@router.get("/{project_id}/tasks/{task_id}", response_model=tasks.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 - - -"""Get users from a specified project""" - -@router.get("/{project_id}/users", response_model=List[users.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() + 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") + return db_project.users +# """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 -## -## POST endpoints -## +# """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 -"""Create a new project""" +# """Get users from a specified project""" -@router.post("/", response_model=projects.ProjectCreate) -def create_project(project: projects.ProjectCreate, db: db_dependency): - db_project = projects.models.Project( - name=project.name, - description=project.description - ) - db.add(db_project) - db.commit() - db.refresh(db_project) - return db_project +# @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 -"""Create a new task in a specified 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 +# ## +# ## POST endpoints +# ## -"""Add users to a specified project using their IDs""" +# """Create a new 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 +# @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 -## -## PUT endpoints -## +# """Create a new task in a specified 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 -"""Update a project by ID""" +# """Add users to a specified project using their IDs""" -@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 +# @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 -"""Update a task in a specified project""" - -@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 - -## -## DELETE endpoints -## - -"""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"} +# ## +# ## PUT endpoints +# ## -"""Delete a task from a specified project""" +# """Update a project by ID""" -@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"} +# @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 -"""Remove users from a specified project using their IDs""" +# """Update a task in a specified project""" -@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"} +# @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 + +# ## +# ## DELETE endpoints +# ## + +# """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"} From dc0c06e1ac32eca61bd9e0503412b91b73d5b7e6 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 2 Feb 2026 13:51:09 +0100 Subject: [PATCH 37/65] Started fixing GET /projects/ --- models.py | 2 +- routers/projects.py | 85 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/models.py b/models.py index 5b2b8a4..74562b0 100644 --- a/models.py +++ b/models.py @@ -38,4 +38,4 @@ class Task(Base): description = Column(String) status = Column(String, default="pending") project_id = Column(Integer, ForeignKey("projects.id")) - project_user = relationship("Project", back_populates="tasks") \ No newline at end of file + project = relationship("Project", back_populates="tasks") \ No newline at end of file diff --git a/routers/projects.py b/routers/projects.py index dff9c62..065b188 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -11,6 +11,8 @@ import schemas.tasks as tasks_schemas import schemas.projects as projects_schemas import schemas.users as users_schemas +from models import Project + router = APIRouter(prefix="/projects", tags=["projects"]) ## @@ -46,9 +48,7 @@ def get_projects(db: db_dependency, request: Request): ) ## fetching projects for the user - projects: projects_schemas.ProjectBase | None = db.query(projects_schemas.ProjectBase).filter(getattr(projects_schemas.ProjectBase, "users.id") == int(user_id)).first() - if projects is None: - return [] + projects = db.query(Project).join(Project.users).filter(getattr(users_schemas.UserBase, "id") == int(user_id)).all() return projects @@ -124,6 +124,85 @@ def get_project_users(project_id: int, request:Request, db: db_dependency): return db_project.users +@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""" + + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + + try: + payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + request.cookies.clear() ## User in cookies not found, clear cookies + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + except JWTError: + request.cookies.clear() ## Probably an invalid token, clear cookies + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) + user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + + 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") + + return db.query(tasks_schemas.TaskBase).filter(getattr(tasks_schemas.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): + """Create a new project""" + + get_token = request.cookies.get("access_token") + if not get_token: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + + try: + payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + user_id: str = str(payload.get("sub")) + if user_id is None: + request.cookies.clear() ## User in cookies not found, clear cookies + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + except JWTError: + request.cookies.clear() ## Probably an invalid token, clear cookies + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) + user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + + db_project = projects_schemas.ProjectCreate( + name=project.name, + description=project.description, + tasks=[], + user_ids=[getattr(user, "id")] + ) + db.add(db_project) + db.commit() + db.refresh(db_project) + + db.commit() + db.refresh(db_project) + + 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): From eccb3b35b4f02e74bc59ae49d359c6743cc25e0e Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 2 Feb 2026 18:32:49 +0100 Subject: [PATCH 38/65] requiring client to be authenticated when searching for user info --- routers/users.py | 76 +++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/routers/users.py b/routers/users.py index 215dd15..47a0a88 100644 --- a/routers/users.py +++ b/routers/users.py @@ -1,46 +1,37 @@ import os from typing import List -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Request +from jose import JWTError, jwt from database import db_dependency import models +from routers import auth import schemas.users as users import schemas.projects as projects from pyargon2 import hash -import pyargon2 router = APIRouter(prefix="/users", tags=["users"]) -"""Get a user by ID""" @router.get("/{user_id}", response_model=users.UserBase) -def read_user(user_id: int, db: db_dependency): +def read_user(user_id: int, db: db_dependency, request:Request): + """Get a user by ID""" + + check_for_valid_token(request, db) + db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") return db_user - -"""Update a user by ID""" - -@router.put("/{user_id}", response_model=users.UserBase) -def update_user(user_id: int, user: users.UserBase, db: db_dependency): - db_user = db.query(models.User).filter(models.User.id == user_id).first() - if db_user is None: - raise HTTPException(status_code=404, detail="User not found") - setattr(db_user, "name", user.name) - setattr(db_user, "email", user.email) - db.commit() - db.refresh(db_user) - return db_user - - -"""Get projects assigned to a user""" - @router.get("/{user_id}/projects", response_model=List[projects.ProjectBase]) -def read_projects_from_user(user_id: int, db: db_dependency): +def read_projects_from_user(user_id: int, db: db_dependency, request: Request): + """Get projects assigned to a user""" + + check_for_valid_token(request, db) + db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") @@ -50,12 +41,10 @@ def read_projects_from_user(user_id: int, db: db_dependency): ## POST endpoints ## - -"""Create a new user""" - @router.post("/", response_model=users.UserBase) def create_user(user: users.UserCreate, db: db_dependency): - + """Create a new user""" + user_salt = os.urandom(32).hex() print("Generated salt:", user_salt) @@ -82,3 +71,38 @@ def delete_user(user_id: int, db: db_dependency): db.commit() return {"detail": "User deleted"} + +def check_for_valid_token(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: + raise HTTPException( + status_code=401, + detail="Not logged in" + ) + try: + payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.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" + ) + 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" + ) + return db_user + + except JWTError: + request.cookies.clear() ## removing invalid auth cookie + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) + + From 057797c07dc9670390e7efd474048b51307c7fb6 Mon Sep 17 00:00:00 2001 From: Edoardo Borgia Leiva Date: Mon, 2 Feb 2026 20:10:43 +0100 Subject: [PATCH 39/65] moved token cheking to auth module --- routers/auth.py | 32 ++++++++++++++++++++++++++++++++ routers/users.py | 34 +--------------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/routers/auth.py b/routers/auth.py index d2d7190..6f294fa 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -27,6 +27,38 @@ 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 check_for_valid_token(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: + 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" + ) + 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" + ) + return db_user + + except JWTError: + request.cookies.clear() ## removing invalid auth cookie + raise HTTPException( + status_code=401, + detail="Could not validate credentials" + ) @router.post("/login") def login(user_data: user_schemas.UserLogin, request: Request, response: Response, db: db_dependency): diff --git a/routers/users.py b/routers/users.py index 47a0a88..74f6128 100644 --- a/routers/users.py +++ b/routers/users.py @@ -9,6 +9,7 @@ import models from routers import auth import schemas.users as users import schemas.projects as projects +from routers.auth import check_for_valid_token from pyargon2 import hash @@ -72,37 +73,4 @@ def delete_user(user_id: int, db: db_dependency): return {"detail": "User deleted"} -def check_for_valid_token(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: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - try: - payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.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" - ) - 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" - ) - return db_user - - except JWTError: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) - From e953e09fee8f66fc770ed91c44190747fb47fec9 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 10:24:00 +0100 Subject: [PATCH 40/65] refactored reused code blocks to use helper function in routers.auth --- routers/auth.py | 6 ++- routers/projects.py | 124 +++----------------------------------------- routers/users.py | 6 +-- 3 files changed, 14 insertions(+), 122 deletions(-) diff --git a/routers/auth.py b/routers/auth.py index 6f294fa..b6c8586 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -16,7 +16,9 @@ SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours -def create_access_token(data: dict, expires_delta: timedelta | None = None): +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 @@ -27,7 +29,7 @@ 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 check_for_valid_token(request: Request, db: db_dependency) -> models.User : +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: diff --git a/routers/projects.py b/routers/projects.py index 065b188..226b26e 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -12,6 +12,7 @@ import schemas.projects as projects_schemas import schemas.users as users_schemas from models import Project +from routers.auth import get_user_from_jwt router = APIRouter(prefix="/projects", tags=["projects"]) @@ -23,29 +24,8 @@ router = APIRouter(prefix="/projects", tags=["projects"]) def get_projects(db: db_dependency, request: Request): """Get a user's projects""" - ## User retrieval from JWT token in cookies - get_token = request.cookies.get("access_token") - if not get_token: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - - try: - payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## User in cookies not found, clear cookies - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## Probably an invalid token, clear cookies - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) + user = get_user_from_jwt(request, db) + 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() @@ -56,29 +36,7 @@ def get_projects(db: db_dependency, request: Request): def get_project(project_id: int, request:Request, db: db_dependency): """Get a project by ID""" - get_token = request.cookies.get("access_token") - if not get_token: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - - try: - payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## User in cookies not found, clear cookies - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## Probably an invalid token, clear cookies - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) - user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + 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: @@ -92,30 +50,7 @@ def get_project(project_id: int, request:Request, db: db_dependency): def get_project_users(project_id: int, request:Request, db: db_dependency): """Get users from a specified project""" - get_token = request.cookies.get("access_token") - if not get_token: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - - try: - payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## User in cookies not found, clear cookies - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## Probably an invalid token, clear cookies - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) - user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() - + 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") @@ -128,29 +63,7 @@ def get_project_users(project_id: int, request:Request, db: db_dependency): def get_project_tasks(project_id: int, request:Request, db: db_dependency): """Get tasks from a specified project""" - get_token = request.cookies.get("access_token") - if not get_token: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - - try: - payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## User in cookies not found, clear cookies - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## Probably an invalid token, clear cookies - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) - user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() + 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: @@ -164,30 +77,7 @@ def get_project_tasks(project_id: int, request:Request, db: db_dependency): def create_project(project: projects_schemas.ProjectCreate, request:Request, db: db_dependency): """Create a new project""" - get_token = request.cookies.get("access_token") - if not get_token: - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - - try: - payload = jwt.decode(get_token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## User in cookies not found, clear cookies - raise HTTPException( - status_code=401, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## Probably an invalid token, clear cookies - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) - user = db.query(users_schemas.UserBase).filter(getattr(users_schemas.UserBase, "id") == user_id).first() - + user = get_user_from_jwt(request, db) db_project = projects_schemas.ProjectCreate( name=project.name, description=project.description, diff --git a/routers/users.py b/routers/users.py index 74f6128..3e29d33 100644 --- a/routers/users.py +++ b/routers/users.py @@ -9,7 +9,7 @@ import models from routers import auth import schemas.users as users import schemas.projects as projects -from routers.auth import check_for_valid_token +from routers.auth import get_user_from_jwt from pyargon2 import hash @@ -20,7 +20,7 @@ router = APIRouter(prefix="/users", tags=["users"]) def read_user(user_id: int, db: db_dependency, request:Request): """Get a user by ID""" - check_for_valid_token(request, db) + get_user_from_jwt(request, db) db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: @@ -31,7 +31,7 @@ def read_user(user_id: int, db: db_dependency, request:Request): def read_projects_from_user(user_id: int, db: db_dependency, request: Request): """Get projects assigned to a user""" - check_for_valid_token(request, db) + get_user_from_jwt(request, db) db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: From aadc5ba45f858beff2ee6e23b9dd75caaed50fc0 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 10:40:54 +0100 Subject: [PATCH 41/65] Added some tags to get_me() --- routers/me.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/me.py b/routers/me.py index 1ab8509..f554cf6 100644 --- a/routers/me.py +++ b/routers/me.py @@ -8,7 +8,7 @@ import schemas.users as user_schemas router = APIRouter(prefix="/me", tags=["me"]) -@router.get("/", response_model=user_schemas.UserBase) +@router.get("/", response_model=user_schemas.UserBase, tags=["me", "users"]) def get_me(request: Request, db: db_dependency): """Get current authenticated user""" token = request.cookies.get("access_token") From 74a2174bb2a787aa68ded019a3bc21c5f3551d55 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 10:47:24 +0100 Subject: [PATCH 42/65] Created new schema projects_users.py to avoid circular imports --- routers/me.py | 36 ++++-------------------------------- schemas/projects_users.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 32 deletions(-) create mode 100644 schemas/projects_users.py diff --git a/routers/me.py b/routers/me.py index f554cf6..54ba96e 100644 --- a/routers/me.py +++ b/routers/me.py @@ -5,10 +5,11 @@ import models from routers import auth import schemas.users as user_schemas +import schemas.projects_users as projects_users_schemas router = APIRouter(prefix="/me", tags=["me"]) -@router.get("/", response_model=user_schemas.UserBase, tags=["me", "users"]) +@router.get("/", response_model=projects_users_schemas.ProjectUserBase, tags=["me", "users"]) def get_me(request: Request, db: db_dependency): """Get current authenticated user""" token = request.cookies.get("access_token") @@ -62,37 +63,8 @@ def logout(request: Request,response: Response): @router.delete("/delete-me", tags=["me", "auth", "users"]) def delete_me(request: Request, db: db_dependency): """Delete current authenticated user""" - token = request.cookies.get("access_token") - - if not token: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not logged in" - ) - - try: - payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not logged in" - ) - except JWTError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials" - ) - -## User retrieval and deletion - user = db.query(models.User).filter(models.User.id == int(user_id)).first() - if user is None: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) + + user = auth.get_user_from_jwt(request, db) db.delete(user) db.commit() diff --git a/schemas/projects_users.py b/schemas/projects_users.py new file mode 100644 index 0000000..4ea25a2 --- /dev/null +++ b/schemas/projects_users.py @@ -0,0 +1,10 @@ +from typing import List + +from pydantic import ConfigDict +from schemas.projects import ProjectBase +from schemas.users import UserBase + +class ProjectUserBase(UserBase): + model_config = ConfigDict(from_attributes=True) + + projects: List[ProjectBase] From 8fb4ba71b951c0b2237704266f4caa44231c9693 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 10:51:09 +0100 Subject: [PATCH 43/65] More refactoring using get_user_from_jwt() --- routers/me.py | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/routers/me.py b/routers/me.py index 54ba96e..f06bb9c 100644 --- a/routers/me.py +++ b/routers/me.py @@ -12,38 +12,8 @@ router = APIRouter(prefix="/me", tags=["me"]) @router.get("/", response_model=projects_users_schemas.ProjectUserBase, tags=["me", "users"]) def get_me(request: Request, db: db_dependency): """Get current authenticated user""" - token = request.cookies.get("access_token") - - if not token: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not logged in" - ) - - try: - payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) - user_id: str = str(payload.get("sub")) - if user_id is None: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not logged in" - ) - except JWTError: - request.cookies.clear() ## removing invalid auth cookie - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials" - ) - - 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=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) - return db_user + user = auth.get_user_from_jwt(request, db) + return user @router.get("/logout", tags=["me", "auth"]) @@ -65,10 +35,8 @@ def delete_me(request: Request, db: db_dependency): """Delete current authenticated user""" user = auth.get_user_from_jwt(request, db) - db.delete(user) db.commit() - ## Logout user by clearing cookie request.cookies.clear() return {"message": "User deleted successfully"} From 96d833a089cb146b50121d9c4a1e5ce70b71156d Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 3 Feb 2026 11:20:19 +0100 Subject: [PATCH 44/65] 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 45/65] 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 46/65] 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 47/65] 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 48/65] 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 49/65] 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 50/65] 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 51/65] 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 52/65] 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 53/65] 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 54/65] 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 55/65] 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 56/65] 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 57/65] 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) From a40296929ad431c2a0f22a9f334c6b7058778273 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Fri, 6 Feb 2026 11:55:14 +0100 Subject: [PATCH 58/65] Fixed project creation endpoint --- routers/projects.py | 18 ++++++------------ schemas/projects.py | 3 ++- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 874c483..6621af2 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -100,19 +100,18 @@ def create_project(project: ProjectCreate, request:Request, db: db_dependency): user = get_user_from_jwt(request, db) - db_project = ProjectCreate( + db_project = Project( name=project.name, description=project.description, - tasks=[], - user_ids=[getattr(user, "id")] + tasks=[] ) + + db_project.users.append(user) + db.add(db_project) db.commit() db.refresh(db_project) - db.commit() - db.refresh(db_project) - return db_project @@ -207,16 +206,11 @@ def delete_project(project_id: int, db: db_dependency, request: Request): 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() + tasks: List[Task] = db.query(Task).filter(Task.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"} diff --git a/schemas/projects.py b/schemas/projects.py index 302d7f5..592cea8 100644 --- a/schemas/projects.py +++ b/schemas/projects.py @@ -16,10 +16,11 @@ class ProjectFull(ProjectBase): id: int class ProjectCreate(BaseModel): + model_config = ConfigDict(from_attributes=True) + name: str description: Optional[str] = None tasks: List[TaskBase] = [] - user_ids: List[int] = [] class ProjectUpdate(BaseModel): name: Optional[str] = None From ac21fceffc236a06db2e8aa99ffff8cfecff1097 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Mon, 9 Feb 2026 13:44:52 +0100 Subject: [PATCH 59/65] made get_projects() return full project info --- routers/projects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index 6621af2..be25752 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -4,7 +4,7 @@ from typing import List, Annotated from database import db_dependency from schemas.tasks import TaskBase, TaskCreate, TaskUpdate -from schemas.projects import ProjectBase, ProjectCreate, ProjectUpdate, ProjectAddUsers, ProjectRemoveUsers +from schemas.projects import ProjectBase, ProjectCreate, ProjectUpdate, ProjectAddUsers, ProjectRemoveUsers, ProjectFull from schemas.users import UserBase from schemas.projects_users import ProjectUserBase from schemas.projects_tasks import ProjectTaskBase, ProjectTaskCreate @@ -34,7 +34,7 @@ def get_task_by_id_for_project(project: ProjectBase, task_id: int, db: db_depend router = APIRouter(prefix="/projects", tags=["projects"]) -@router.get("/", response_model=List[ProjectBase], tags=["projects", "me"]) +@router.get("/", response_model=List[ProjectFull], tags=["projects", "me"]) def get_projects(db: db_dependency, request: Request): """Get a user's projects""" @@ -46,7 +46,7 @@ def get_projects(db: db_dependency, request: Request): return projects -@router.get("/{project_id}", response_model=ProjectBase) +@router.get("/{project_id}", response_model=ProjectFull) def get_project(project_id: int, request:Request, db: db_dependency): """Get a project by ID""" From 839728cf6604d2c27f560552b7729b2f072e5c71 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 11:37:30 +0100 Subject: [PATCH 60/65] Made ProjectUser return full project info --- schemas/projects_users.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/schemas/projects_users.py b/schemas/projects_users.py index 4ea25a2..36c74fd 100644 --- a/schemas/projects_users.py +++ b/schemas/projects_users.py @@ -1,10 +1,11 @@ from typing import List from pydantic import ConfigDict -from schemas.projects import ProjectBase +from schemas.projects import ProjectFull from schemas.users import UserBase class ProjectUserBase(UserBase): model_config = ConfigDict(from_attributes=True) - projects: List[ProjectBase] + projects: List[ProjectFull] + \ No newline at end of file From 48b2bcfa7007ab00659f1e620fa0a00b86737552 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 11:37:59 +0100 Subject: [PATCH 61/65] Refactored me.py, minor changes --- routers/me.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/routers/me.py b/routers/me.py index 44f78fe..b5cd2b3 100644 --- a/routers/me.py +++ b/routers/me.py @@ -4,12 +4,15 @@ from jose import JWTError, jwt import models from routers import auth -import schemas.users as user_schemas -import schemas.projects_users as projects_users_schemas + +from schemas.users import UserBase +from schemas.projects import ProjectBase +from schemas.projects_users import ProjectUserBase + router = APIRouter(prefix="/me", tags=["me"]) -@router.get("/", response_model=projects_users_schemas.ProjectUserBase, tags=["me", "users"]) +@router.get("/", response_model=ProjectUserBase, tags=["me", "users"]) def get_me(request: Request, db: db_dependency): """Get current authenticated user""" user = auth.get_user_from_jwt(request, db) From 1b84af00253a7d6a34d52f1f1b06ac4f37ddff11 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 11:47:36 +0100 Subject: [PATCH 62/65] Fixed confusion in project creation between sqlalchemy and pydantic model --- routers/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index be25752..db30d18 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -115,14 +115,14 @@ def create_project(project: ProjectCreate, request:Request, db: db_dependency): return db_project -@router.post("/{project_id}/tasks", response_model=ProjectTaskCreate, tags=["tasks"]) +@router.post("/{project_id}/tasks", response_model=ProjectTaskBase, 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 = get_project_by_id_for_user(user, project_id, db) - db_task = ProjectTaskCreate( + db_task = Task( title=task.title, description=task.description, status=task.status, From 0f0b27b2d98a8131094acf03f690652e6377472a Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 12:36:40 +0100 Subject: [PATCH 63/65] change user addition to project to be based off of email address and not user id --- routers/projects.py | 16 ++++++++-------- schemas/projects.py | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index db30d18..c99b5fb 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -4,7 +4,7 @@ from typing import List, Annotated from database import db_dependency from schemas.tasks import TaskBase, TaskCreate, TaskUpdate -from schemas.projects import ProjectBase, ProjectCreate, ProjectUpdate, ProjectAddUsers, ProjectRemoveUsers, ProjectFull +from schemas.projects import ProjectBase, ProjectCreate, ProjectUpdate, ProjectAddUser, ProjectRemoveUsers, ProjectFull from schemas.users import UserBase from schemas.projects_users import ProjectUserBase from schemas.projects_tasks import ProjectTaskBase, ProjectTaskCreate @@ -134,17 +134,17 @@ def create_project_task(project_id: int, task: TaskCreate, db: db_dependency, re db.refresh(db_task) return db_task -@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""" +@router.post("/{project_id}/users", response_model=ProjectAddUser, tags=["users"]) +def add_project_user(project_id: int, user_data: ProjectAddUser, db: db_dependency, request: Request): + """Add a user to a specified project using their email address""" user = get_user_from_jwt(request, db) 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() - if db_user: - db_project.users.append(db_user) + db_user = db.query(UserBase).filter(getattr(UserBase, "email") == user_data.user_email).first() + if db_user: + db_project.users.append(db_user) + db.commit() db.refresh(db_project) return db_project diff --git a/schemas/projects.py b/schemas/projects.py index 592cea8..28c2ad4 100644 --- a/schemas/projects.py +++ b/schemas/projects.py @@ -26,6 +26,9 @@ class ProjectUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None +class ProjectAddUser(BaseModel): + user_email: str + class ProjectAddUsers(BaseModel): user_ids: List[int] = [] From 8d7537b602108edd17f9d93900e72cf7e17468e4 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 13:02:01 +0100 Subject: [PATCH 64/65] Fixed adding collaborator --- routers/projects.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/routers/projects.py b/routers/projects.py index c99b5fb..fc87a81 100644 --- a/routers/projects.py +++ b/routers/projects.py @@ -80,7 +80,7 @@ def get_project_user(project_id: int, user_id: int, db: db_dependency, request: 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() + db_user = db.query(User).filter(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") return db_user @@ -91,7 +91,7 @@ def get_project_tasks(project_id: int, request:Request, db: db_dependency): user = get_user_from_jwt(request, db) 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() + db_tasks = db.query(Task).filter(Task.project_id == project_id).all() return db_tasks @router.post("/", response_model=ProjectCreate) @@ -134,16 +134,21 @@ def create_project_task(project_id: int, task: TaskCreate, db: db_dependency, re db.refresh(db_task) return db_task -@router.post("/{project_id}/users", response_model=ProjectAddUser, tags=["users"]) +@router.post("/{project_id}/users", response_model=ProjectFull, tags=["users"]) def add_project_user(project_id: int, user_data: ProjectAddUser, db: db_dependency, request: Request): """Add a user to a specified project using their email address""" user = get_user_from_jwt(request, db) - db_project = get_project_by_id_for_user(user, project_id, db) - db_user = db.query(UserBase).filter(getattr(UserBase, "email") == user_data.user_email).first() - if db_user: + db_user = db.query(User).filter(User.email == user_data.user_email).first() + + if not db_user: + raise HTTPException(status_code=404, detail="User with the specified email not found") + + if db_user not in db_project.users: db_project.users.append(db_user) + else: + raise HTTPException(status_code=400, detail="User is already a member of the project") db.commit() db.refresh(db_project) @@ -156,7 +161,7 @@ def remove_user_from_project(project_id: int, user_id: int, db: db_dependency, r db_project = get_project_by_id_for_user(user, project_id, db) - db_user = db.query(UserBase).filter(getattr(UserBase, "id") == user_id).first() + db_user = db.query(User).filter(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") From 95d37bd3784617004063e8d9a725e6df28d66a4d Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 13:40:13 +0100 Subject: [PATCH 65/65] Added samsple frontends repos in docs description --- main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main.py b/main.py index ba3776f..da64346 100644 --- a/main.py +++ b/main.py @@ -28,6 +28,13 @@ It allows users to manage projects, tasks, and user assignments with proper auth ## 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). + +## Other projects +Here are some frontend implementations for this API: +- [KanbanCloneAngular](https://github.com/a-mayb3/KanbanCloneAngular) - Angular frontend +- [KanbanCloneAndroid](https://github.com/a-mayb3/KanbanCloneAndroid) - Android frontend + + """ global_logger = logging.getLogger()