from fastapi import APIRouter, HTTPException, Depends, status from typing import List, Optional from pydantic import BaseModel import json import time import logging from .auth_router import get_current_user from app.utils import db_http # Configure logging logger = logging.getLogger("auth-server") router = APIRouter() # Models class ProjectBase(BaseModel): title: str description: Optional[str] = None geojson: Optional[str] = None class ProjectCreate(ProjectBase): pass class ProjectUpdate(ProjectBase): title: Optional[str] = None class ProjectResponse(ProjectBase): id: int owner_id: int storage_bucket: str created_at: str updated_at: str # Routes @router.post("/", response_model=ProjectResponse) async def create_project( project: ProjectCreate, current_user = Depends(get_current_user) ): operation_id = f"create_project_{int(time.time())}" logger.info(f"[{operation_id}] Creating new project: {project.title}") try: # Get user ID based on the type of current_user if isinstance(current_user, dict): user_id = current_user.get("id") else: user_id = current_user[0] logger.info(f"[{operation_id}] User ID: {user_id}") # Validate GeoJSON if provided if project.geojson: try: geojson_data = json.loads(project.geojson) # Basic validation if not isinstance(geojson_data, dict) or "type" not in geojson_data: logger.warning(f"[{operation_id}] Invalid GeoJSON format") raise ValueError("Invalid GeoJSON format") except json.JSONDecodeError: logger.warning(f"[{operation_id}] Invalid GeoJSON format (JSON decode error)") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid GeoJSON format" ) logger.info(f"[{operation_id}] GeoJSON validation passed") # Prepare project data project_data = { "owner_id": user_id, "title": project.title, "description": project.description, "geojson": project.geojson, "storage_bucket": "default" # Default storage bucket } # Insert the new project using HTTP API logger.info(f"[{operation_id}] Inserting new project") project_id = db_http.insert_record("projects", project_data, operation_id=operation_id) if not project_id: logger.error(f"[{operation_id}] Failed to insert project") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create project" ) logger.info(f"[{operation_id}] Project inserted with ID: {project_id}") # Get the newly created project new_project = db_http.get_record_by_id("projects", project_id, operation_id=operation_id) if not new_project: logger.warning(f"[{operation_id}] Project not found after insert, trying by owner and title") # Try to get by owner and title as fallback projects = db_http.select_records( "projects", condition="owner_id = ? AND title = ?", condition_params=[ {"type": "integer", "value": str(user_id)}, {"type": "text", "value": project.title} ], order_by="id DESC", limit=1, operation_id=operation_id ) if projects: new_project = projects[0] project_id = new_project.get("id") logger.info(f"[{operation_id}] Found project by owner and title with ID: {project_id}") else: logger.error(f"[{operation_id}] Project not found after insert") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Project was created but could not be retrieved" ) logger.info(f"[{operation_id}] Project created successfully with ID: {project_id}") return { "id": new_project.get("id"), "owner_id": new_project.get("owner_id"), "title": new_project.get("title"), "description": new_project.get("description"), "geojson": new_project.get("geojson"), "storage_bucket": new_project.get("storage_bucket", "default"), "created_at": new_project.get("created_at", ""), "updated_at": new_project.get("updated_at", "") } except HTTPException: raise except Exception as e: logger.error(f"[{operation_id}] Error creating project: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) @router.get("/", response_model=List[ProjectResponse]) async def get_projects( current_user = Depends(get_current_user), skip: int = 0, limit: int = 100 ): operation_id = f"get_projects_{int(time.time())}" logger.info(f"[{operation_id}] Getting projects (skip={skip}, limit={limit})") try: # Get user ID based on the type of current_user if isinstance(current_user, dict): user_id = current_user.get("id") else: user_id = current_user[0] logger.info(f"[{operation_id}] User ID: {user_id}") # Get projects using HTTP API projects = db_http.select_records( "projects", condition="owner_id = ?", condition_params=[{"type": "integer", "value": str(user_id)}], order_by="updated_at DESC", limit=limit, offset=skip, operation_id=operation_id ) logger.info(f"[{operation_id}] Found {len(projects)} projects") # Projects are already in dictionary format from the HTTP API # Just need to ensure all required fields are present result = [] for project in projects: result.append({ "id": project.get("id"), "owner_id": project.get("owner_id"), "title": project.get("title"), "description": project.get("description"), "geojson": project.get("geojson"), "storage_bucket": project.get("storage_bucket", "default"), "created_at": project.get("created_at", ""), "updated_at": project.get("updated_at", "") }) return result except Exception as e: logger.error(f"[{operation_id}] Error getting projects: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) @router.get("/{project_id}", response_model=ProjectResponse) async def get_project( project_id: int, current_user = Depends(get_current_user) ): operation_id = f"get_project_{int(time.time())}" logger.info(f"[{operation_id}] Getting project with ID: {project_id}") try: # Get user ID based on the type of current_user if isinstance(current_user, dict): user_id = current_user.get("id") else: user_id = current_user[0] logger.info(f"[{operation_id}] User ID: {user_id}") # Get project using HTTP API projects = db_http.select_records( "projects", condition="id = ? AND owner_id = ?", condition_params=[ {"type": "integer", "value": str(project_id)}, {"type": "integer", "value": str(user_id)} ], limit=1, operation_id=operation_id ) if not projects: logger.warning(f"[{operation_id}] Project not found with ID: {project_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" ) project = projects[0] logger.info(f"[{operation_id}] Found project: {project.get('title')}") return { "id": project.get("id"), "owner_id": project.get("owner_id"), "title": project.get("title"), "description": project.get("description"), "geojson": project.get("geojson"), "storage_bucket": project.get("storage_bucket", "default"), "created_at": project.get("created_at", ""), "updated_at": project.get("updated_at", "") } except HTTPException: raise except Exception as e: logger.error(f"[{operation_id}] Error getting project: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) @router.patch("/{project_id}", response_model=ProjectResponse) async def update_project( project_id: int, project_update: ProjectUpdate, current_user = Depends(get_current_user) ): operation_id = f"update_project_{int(time.time())}" logger.info(f"[{operation_id}] Updating project with ID: {project_id}") try: # Get user ID based on the type of current_user if isinstance(current_user, dict): user_id = current_user.get("id") else: user_id = current_user[0] logger.info(f"[{operation_id}] User ID: {user_id}") # Check if project exists and belongs to user projects = db_http.select_records( "projects", condition="id = ? AND owner_id = ?", condition_params=[ {"type": "integer", "value": str(project_id)}, {"type": "integer", "value": str(user_id)} ], limit=1, operation_id=operation_id ) if not projects: logger.warning(f"[{operation_id}] Project not found with ID: {project_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" ) existing_project = projects[0] logger.info(f"[{operation_id}] Found project: {existing_project.get('title')}") # Prepare update data update_data = {} if project_update.title is not None: update_data["title"] = project_update.title if project_update.description is not None: update_data["description"] = project_update.description if project_update.geojson is not None: # Validate GeoJSON try: geojson_data = json.loads(project_update.geojson) if not isinstance(geojson_data, dict) or "type" not in geojson_data: logger.warning(f"[{operation_id}] Invalid GeoJSON format") raise ValueError("Invalid GeoJSON format") except json.JSONDecodeError: logger.warning(f"[{operation_id}] Invalid GeoJSON format (JSON decode error)") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid GeoJSON format" ) update_data["geojson"] = project_update.geojson # Add updated_at field update_data["updated_at"] = time.strftime('%Y-%m-%d %H:%M:%S') # If no fields to update, return the existing project if len(update_data) <= 1: # Only updated_at logger.info(f"[{operation_id}] No fields to update") return { "id": existing_project.get("id"), "owner_id": existing_project.get("owner_id"), "title": existing_project.get("title"), "description": existing_project.get("description"), "geojson": existing_project.get("geojson"), "storage_bucket": existing_project.get("storage_bucket", "default"), "created_at": existing_project.get("created_at", ""), "updated_at": existing_project.get("updated_at", "") } # Update the project using HTTP API logger.info(f"[{operation_id}] Updating project with data: {update_data}") success = db_http.update_record( "projects", update_data, "id = ? AND owner_id = ?", [ {"type": "integer", "value": str(project_id)}, {"type": "integer", "value": str(user_id)} ], operation_id=operation_id ) if not success: logger.error(f"[{operation_id}] Failed to update project") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update project" ) logger.info(f"[{operation_id}] Project updated successfully") # Get the updated project updated_project = db_http.get_record_by_id("projects", project_id, operation_id=operation_id) if not updated_project: logger.warning(f"[{operation_id}] Updated project not found") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Project was updated but could not be retrieved" ) return { "id": updated_project.get("id"), "owner_id": updated_project.get("owner_id"), "title": updated_project.get("title"), "description": updated_project.get("description"), "geojson": updated_project.get("geojson"), "storage_bucket": updated_project.get("storage_bucket", "default"), "created_at": updated_project.get("created_at", ""), "updated_at": updated_project.get("updated_at", "") } except HTTPException: raise except Exception as e: logger.error(f"[{operation_id}] Error updating project: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_project( project_id: int, current_user = Depends(get_current_user) ): operation_id = f"delete_project_{int(time.time())}" logger.info(f"[{operation_id}] Deleting project with ID: {project_id}") try: # Get user ID based on the type of current_user if isinstance(current_user, dict): user_id = current_user.get("id") else: user_id = current_user[0] logger.info(f"[{operation_id}] User ID: {user_id}") # Check if project exists and belongs to user projects = db_http.select_records( "projects", condition="id = ? AND owner_id = ?", condition_params=[ {"type": "integer", "value": str(project_id)}, {"type": "integer", "value": str(user_id)} ], limit=1, operation_id=operation_id ) if not projects: logger.warning(f"[{operation_id}] Project not found with ID: {project_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" ) logger.info(f"[{operation_id}] Found project to delete: {projects[0].get('title')}") # Delete the project using HTTP API success = db_http.delete_record( "projects", "id = ?", [{"type": "integer", "value": str(project_id)}], operation_id=operation_id ) if not success: logger.error(f"[{operation_id}] Failed to delete project") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete project" ) logger.info(f"[{operation_id}] Project deleted successfully") return None except HTTPException: raise except Exception as e: logger.error(f"[{operation_id}] Error deleting project: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) )