diff --git a/.gitignore b/.gitignore index 784b2bb..b7faf40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,3 @@ -*.db -log.app - -.idea/ -.vscode/ - -.env* - # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/database.py b/database.py deleted file mode 100644 index 3c253b3..0000000 --- a/database.py +++ /dev/null @@ -1,32 +0,0 @@ - -import sqlalchemy -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, echo=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() - -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() - 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 deleted file mode 100644 index da64346..0000000 --- a/main.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging -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_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). - -## 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() -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) -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 - -app = FastAPI( - lifespan=lifespan, - 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( - 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 :)""" -@app.get("/ping") -def ping(): - return {"message": "pong"} - -"""Gives project url""" -@app.get("/sources") -def source(): - return {"url": "https://github.com/a-mayb3/Kanban_clone_backend"} - -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""" - - logger = global_logger - logger.error(f"HTTP error occurred: {exc.detail}") - - 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""" - - logger = global_logger - logger.error(f"Validation error: {exc.errors()}") - - return JSONResponse( - status_code=422, - content={ - "error": { - "message": "Validation error", - "type": "validation_error", - "details": exc.errors() - } - } - ) - -@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 diff --git a/models.py b/models.py deleted file mode 100644 index 74562b0..0000000 --- a/models.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy import Column, ForeignKey, String, Integer, Table -from sqlalchemy.dialects.sqlite import BLOB -from sqlalchemy.orm import relationship -from database import Base -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, autoincrement=True) - name = Column(String, index=True) - email = Column(String, unique=True, index=True) - 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, autoincrement=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, autoincrement=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 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cc52ab5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,58 +0,0 @@ -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 -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 -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 -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 -urllib3==2.6.3 -uvicorn==0.40.0 -uvloop==0.22.1 -watchfiles==1.1.1 -websockets==16.0 diff --git a/routers/auth.py b/routers/auth.py deleted file mode 100644 index 1264294..0000000 --- a/routers/auth.py +++ /dev/null @@ -1,142 +0,0 @@ -import os - -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 -import models - -import schemas.users as user_schemas - -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): - """Create a JWT token""" - - 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 - -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""" - - 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: - 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="Could not verify credentials" - ) - return db_user - 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 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): - """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: - 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( - 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 - ) - - # 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 - } - } - diff --git a/routers/me.py b/routers/me.py deleted file mode 100644 index b5cd2b3..0000000 --- a/routers/me.py +++ /dev/null @@ -1,57 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Response, Request -from database import db_dependency -from jose import JWTError, jwt -import models - -from routers import auth - -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=ProjectUserBase, tags=["me", "users"]) -def get_me(request: Request, db: db_dependency): - """Get current authenticated user""" - user = auth.get_user_from_jwt(request, db) - return user - - -@router.get("/logout", tags=["me", "auth"]) -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", tags=["me", "auth", "users"]) -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 - request.cookies.clear() - return {"message": "User deleted successfully"} diff --git a/routers/projects.py b/routers/projects.py deleted file mode 100644 index fc87a81..0000000 --- a/routers/projects.py +++ /dev/null @@ -1,234 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends, Request -from typing import List, Annotated - -from database import db_dependency - -from schemas.tasks import TaskBase, TaskCreate, TaskUpdate -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 - -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(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: - 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(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 - -router = APIRouter(prefix="/projects", tags=["projects"]) - -@router.get("/", response_model=List[ProjectFull], tags=["projects", "me"]) -def get_projects(db: db_dependency, request: Request): - """Get a user's projects""" - - 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(User.id == user_id).all() - return projects - - -@router.get("/{project_id}", response_model=ProjectFull) -def get_project(project_id: int, request:Request, db: db_dependency): - """Get a project by ID""" - - user = get_user_from_jwt(request, 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): - """Get users from a specified project""" - - user = get_user_from_jwt(request, db) - 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) - - return db_task - -@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) - - db_project : ProjectBase = get_project_by_id_for_user(user, project_id, db) - - 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 - -@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 = get_project_by_id_for_user(user, project_id, db) - db_tasks = db.query(Task).filter(Task.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 = Project( - name=project.name, - description=project.description, - tasks=[] - ) - - db_project.users.append(user) - - db.add(db_project) - db.commit() - db.refresh(db_project) - - return db_project - - -@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 = Task( - 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("/{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(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) - return db_project - -@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 = get_project_by_id_for_user(user, project_id, db) - - 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") - - db_project.users.remove(db_user) - db.commit() - db.refresh(db_project) - return db_project - -@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 = get_project_by_id_for_user(user, project_id, db) - db_task = get_task_by_id_for_project(db_project, task_id, db) - - 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.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 = 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 - -@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) - db_project = get_project_by_id_for_user(user, project_id, db) - - ## Remove dangling tasks and user associations - 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) - - db.delete(db_project) - db.commit() - return {"detail": "Project deleted successfully"} - -@router.delete("/{project_id}/tasks/{task_id}" , tags=["tasks"]) -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 = 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_project.tasks.remove(db_task) - - db.commit() - return {"detail": "Task deleted successfully"} diff --git a/routers/users.py b/routers/users.py deleted file mode 100644 index 3e29d33..0000000 --- a/routers/users.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -from typing import List -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 routers.auth import get_user_from_jwt - -from pyargon2 import hash - -router = APIRouter(prefix="/users", tags=["users"]) - - -@router.get("/{user_id}", response_model=users.UserBase) -def read_user(user_id: int, db: db_dependency, request:Request): - """Get a user by ID""" - - get_user_from_jwt(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 - -@router.get("/{user_id}/projects", response_model=List[projects.ProjectBase]) -def read_projects_from_user(user_id: int, db: db_dependency, request: Request): - """Get projects assigned to a user""" - - get_user_from_jwt(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.projects - -## -## POST endpoints -## - -@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) - - hashed_password = hash(password=user.password, salt=user_salt, variant="id") - - db_user = models.User( - name=user.name, - email=user.email, - password_hash=hashed_password, - password_salt=user_salt - ) - - db.add(db_user) - db.commit() - db.refresh(db_user) - 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/projects.py b/schemas/projects.py deleted file mode 100644 index 28c2ad4..0000000 --- a/schemas/projects.py +++ /dev/null @@ -1,36 +0,0 @@ -from pydantic import BaseModel, ConfigDict -from typing import List, Optional - -from schemas.tasks import TaskBase -from schemas.users import UserBase - -class ProjectBase(BaseModel): - model_config = ConfigDict(from_attributes=True) - - name: str - description: str - tasks: List[TaskBase] - users: List[UserBase] - -class ProjectFull(ProjectBase): - id: int - -class ProjectCreate(BaseModel): - model_config = ConfigDict(from_attributes=True) - - name: str - description: Optional[str] = None - tasks: List[TaskBase] = [] - -class ProjectUpdate(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - -class ProjectAddUser(BaseModel): - user_email: str - -class ProjectAddUsers(BaseModel): - user_ids: List[int] = [] - -class ProjectRemoveUsers(BaseModel): - user_ids: List[int] = [] diff --git a/schemas/projects_tasks.py b/schemas/projects_tasks.py deleted file mode 100644 index 9f97c28..0000000 --- a/schemas/projects_tasks.py +++ /dev/null @@ -1,11 +0,0 @@ -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/projects_users.py b/schemas/projects_users.py deleted file mode 100644 index 36c74fd..0000000 --- a/schemas/projects_users.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List - -from pydantic import ConfigDict -from schemas.projects import ProjectFull -from schemas.users import UserBase - -class ProjectUserBase(UserBase): - model_config = ConfigDict(from_attributes=True) - - projects: List[ProjectFull] - \ No newline at end of file diff --git a/schemas/tasks.py b/schemas/tasks.py deleted file mode 100644 index 12f7d81..0000000 --- a/schemas/tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -from enum import Enum -from pydantic import BaseModel, ConfigDict -from typing import List, Annotated, Optional - -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 TaskCreate(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 deleted file mode 100644 index 1d663da..0000000 --- a/schemas/users.py +++ /dev/null @@ -1,26 +0,0 @@ -from pydantic import BaseModel, ConfigDict -from typing import List, Optional - -class UserBase(BaseModel): - model_config = ConfigDict(from_attributes=True) - - id: int - name: str - email: str - -class UserCreate(BaseModel): - name: str - email: str - password: str - -class UserUpdateInfo(BaseModel): - name: Optional[str] = None - email: Optional[str] = None - -class UserUpdatePassword(BaseModel): - password: str - new_password: str - -class UserLogin(BaseModel): - email: str - password: str