""" Task CRUD API endpoints. """ from typing import List from fastapi import APIRouter, Depends, HTTPException, status, Query, Header from sqlmodel import Session from src.db.session import get_session from src.models.task import TaskCreate, TaskUpdate, TaskResponse, Task from src.models.user import User from src.services.task_service import TaskService from src.services.user_registration_service import user_registration_service from src.middleware.auth import get_current_user router = APIRouter(prefix="/tasks", tags=["tasks"]) def get_or_create_backend_user( session: Session, user_id: str, user_email: str = None, user_name: str = None ) -> str: """ Get or create backend user from Better Auth user info. Args: session: Database session user_id: Better Auth user ID (email or UUID) user_email: User email user_name: User name Returns: str: String user ID (always VARCHAR) Note: Always returns string ID for Better Auth compatibility """ try: # Ensure user_id is not None and is string if not user_id or not isinstance(user_id, str): if user_email: user_id = user_email else: raise ValueError("user_id or user_email must be provided") user = user_registration_service.get_or_create_user( session=session, user_id=str(user_id), # Always convert to string email=user_email, name=user_name ) return str(user.id) # Ensure return is string except Exception as e: import logging logger = logging.getLogger(__name__) logger.error(f"Error in get_or_create_backend_user: {e}", exc_info=True) raise @router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED) def create_task( task_data: TaskCreate, user_id: str = Query(None, description="User ID for demo/unauthenticated users"), x_user_email: str = Header(None, alias="X-User-Email"), x_user_name: str = Header(None, alias="X-User-Name"), session: Session = Depends(get_session), current_user: User = Depends(lambda: None) # Make auth optional ): """ Create a new task for the authenticated user or demo user. Args: task_data: Task creation data (subitems or shopping_list as JSON array) user_id: Optional user ID for demo users (if not authenticated) x_user_email: User email from Better Auth (header) x_user_name: User name from Better Auth (header) session: Database session current_user: Authenticated user (optional) Returns: TaskResponse: Created task Raises: HTTPException: If validation or creation fails """ import logging logger = logging.getLogger(__name__) try: # Determine user ID: use authenticated user if available, otherwise use provided user_id if current_user and hasattr(current_user, 'id'): effective_user_id = str(current_user.id) elif user_id: # Auto-register Better Auth user and get backend string ID effective_user_id = get_or_create_backend_user( session=session, user_id=user_id, user_email=x_user_email, user_name=x_user_name ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required or user_id must be provided" ) logger.info(f"Creating task for user: {effective_user_id}, title: {task_data.title}") task = TaskService.create_task(session, effective_user_id, task_data) logger.info(f"Task created successfully: ID={task.id}") return TaskResponse(**task.model_dump()) except ValueError as e: logger.error(f"Validation error: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error(f"Error creating task: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create task" ) @router.get("", response_model=List[TaskResponse]) def get_tasks( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=200), user_id: str = Query(None, description="User ID for demo/unauthenticated users"), x_user_email: str = Header(None, alias="X-User-Email"), x_user_name: str = Header(None, alias="X-User-Name"), session: Session = Depends(get_session), current_user: User = Depends(lambda: None) # Make auth optional ): """ Get all tasks for the authenticated user or demo user with pagination. Args: skip: Number of records to skip limit: Maximum number of records to return user_id: Optional user ID for demo users (if not authenticated) x_user_email: User email from Better Auth (header) x_user_name: User name from Better Auth (header) session: Database session current_user: Authenticated user (optional) Returns: List[TaskResponse]: List of user's tasks """ # Determine user ID: use authenticated user if available, otherwise use provided user_id if current_user and hasattr(current_user, 'id'): effective_user_id = current_user.id elif user_id: # Auto-register Better Auth user and get backend integer ID effective_user_id = get_or_create_backend_user( session=session, user_id=user_id, user_email=x_user_email, user_name=x_user_name ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required or user_id must be provided" ) tasks = TaskService.get_tasks(session, effective_user_id, skip, limit) return [TaskResponse(**task.model_dump()) for task in tasks] @router.get("/{task_id}", response_model=TaskResponse) def get_task( task_id: str, user_id: str = Query(None, description="User ID for demo/unauthenticated users"), x_user_email: str = Header(None, alias="X-User-Email"), x_user_name: str = Header(None, alias="X-User-Name"), session: Session = Depends(get_session), current_user: User = Depends(lambda: None) # Make auth optional ): """ Get a specific task by ID (either server ID or client ID). Args: task_id: Task ID (can be integer ID or string client_id) user_id: Optional user ID for demo users (if not authenticated) x_user_email: User email from Better Auth (header) x_user_name: User name from Better Auth (header) session: Database session current_user: Authenticated user (optional) Returns: TaskResponse: Requested task Raises: HTTPException: If task not found or doesn't belong to user """ # Determine user ID: use authenticated user if available, otherwise use provided user_id if current_user and hasattr(current_user, 'id'): effective_user_id = current_user.id elif user_id: # Auto-register Better Auth user and get backend integer ID effective_user_id = get_or_create_backend_user( session=session, user_id=user_id, user_email=x_user_email, user_name=x_user_name ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required or user_id must be provided" ) # Check if task_id is an integer (server ID) or string (client ID) try: # Try parsing as integer first task_id_int = int(task_id) task = TaskService.get_task_by_id(session, task_id_int, effective_user_id) except ValueError: # If it's not an integer, treat as client_id task = TaskService.get_task_by_client_id(session, task_id, effective_user_id) if not task: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" ) return TaskResponse(**task.model_dump()) @router.put("/{task_id}", response_model=TaskResponse) def update_task( task_id: str, task_data: TaskUpdate, user_id: str = Query(None, description="User ID for demo/unauthenticated users"), x_user_email: str = Header(None, alias="X-User-Email"), x_user_name: str = Header(None, alias="X-User-Name"), session: Session = Depends(get_session), current_user: User = Depends(lambda: None) # Make auth optional ): """ Update a task (by either server ID or client ID). Args: task_id: Task ID to update (can be integer ID or string client_id) task_data: Updated task data (subitems or shopping_list as JSON array) user_id: Optional user ID for demo users (if not authenticated) x_user_email: User email from Better Auth (header) x_user_name: User name from Better Auth (header) session: Database session current_user: Authenticated user (optional) Returns: TaskResponse: Updated task Raises: HTTPException: If task not found or doesn't belong to user """ import logging logger = logging.getLogger(__name__) try: # Determine user ID: use authenticated user if available, otherwise use provided user_id if current_user and hasattr(current_user, 'id'): effective_user_id = str(current_user.id) elif user_id: # Auto-register Better Auth user and get backend string ID effective_user_id = get_or_create_backend_user( session=session, user_id=user_id, user_email=x_user_email, user_name=x_user_name ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required or user_id must be provided" ) logger.info(f"Updating task {task_id} for user: {effective_user_id}") # Check if task_id is an integer (server ID) or string (client ID) try: # Try parsing as integer first task_id_int = int(task_id) task = TaskService.update_task(session, task_id_int, effective_user_id, task_data) except ValueError: # If it's not an integer, treat as client_id # For client_id, we need to first get the task to get its server ID task_obj = TaskService.get_task_by_client_id(session, task_id, effective_user_id) if not task_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" ) task = TaskService.update_task(session, task_obj.id, effective_user_id, task_data) if not task: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" ) logger.info(f"Task updated successfully: ID={task.id}") return TaskResponse(**task.model_dump()) except HTTPException: raise except ValueError as e: logger.error(f"Validation error: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error(f"Error updating task: {e}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update task" ) @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_task( task_id: str, user_id: str = Query(None, description="User ID for demo/unauthenticated users"), x_user_email: str = Header(None, alias="X-User-Email"), x_user_name: str = Header(None, alias="X-User-Name"), session: Session = Depends(get_session), current_user: User = Depends(lambda: None) # Make auth optional ): """ Delete a task (by either server ID or client ID). Args: task_id: Task ID to delete (can be integer ID or string client_id) user_id: Optional user ID for demo users (if not authenticated) x_user_email: User email from Better Auth (header) x_user_name: User name from Better Auth (header) session: Database session current_user: Authenticated user (optional) Raises: HTTPException: If task not found or doesn't belong to user """ # Determine user ID: use authenticated user if available, otherwise use provided user_id if current_user and hasattr(current_user, 'id'): effective_user_id = current_user.id elif user_id: # Auto-register Better Auth user and get backend integer ID effective_user_id = get_or_create_backend_user( session=session, user_id=user_id, user_email=x_user_email, user_name=x_user_name ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required or user_id must be provided" ) # Check if task_id is an integer (server ID) or string (client ID) try: # Try parsing as integer first task_id_int = int(task_id) deleted = TaskService.delete_task(session, task_id_int, effective_user_id) except ValueError: # If it's not an integer, treat as client_id # For client_id, we need to first get the task to get its server ID task_obj = TaskService.get_task_by_client_id(session, task_id, effective_user_id) if not task_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" ) deleted = TaskService.delete_task(session, task_obj.id, effective_user_id) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" )