diff --git a/backend-go/internal/controller/found.go b/backend-go/internal/controller/found.go index ae785ce..9a7eb8d 100644 --- a/backend-go/internal/controller/found.go +++ b/backend-go/internal/controller/found.go @@ -1,290 +1,29 @@ package controller import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strconv" - - "github.com/LambdaIITH/Dashboard/backend/config" - found "github.com/LambdaIITH/Dashboard/backend/internal/db" - helpers "github.com/LambdaIITH/Dashboard/backend/internal/helpers" - schema "github.com/LambdaIITH/Dashboard/backend/internal/schema" - "github.com/gin-gonic/gin" ) func AddFoundItemHandler(c *gin.Context) { - // Parsing the form data - formData := c.PostForm("form_data") - var formDataDict map[string]interface{} - if err := json.Unmarshal([]byte(formData), &formDataDict); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) - return - } - - // Get and Verify the user ID - userId, err := helpers.GetUserID(c) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) - return - } - - // Insert the form data into the found table - result, err := found.InsertInFoundTable(c, formDataDict, userId) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert data"}) - return - } - - fmt.Println("Inserted data:", result) - - // Upload the images to S3 and save the image URLs in the database - form, err := c.MultipartForm() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file data"}) - return - } - - files := form.File["images"] - if len(files) > 0 { - s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) - - imagePaths, err := s3Client.UploadImages(files, result["id"].(int), "found") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload images"}) - return - } - - err = found.InsertFoundImages(c, imagePaths, result["id"].(int)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image paths"}) - return - } - } - c.JSON(http.StatusOK, gin.H{"message": "Data inserted successfully"}) + AddItemGenericHandler(c, "found", "found_images") } func GetAllFoundItemsHandler(c *gin.Context) { - // Fetch all the found items - items, err := found.GetAllFoundItems(c) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch items"}) - return - } - - // Fetch the image URLs associated with the items - rows, err := config.DB.Query(c, "SELECT item_id, image_url FROM found_images") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch images"}) - return - } - defer rows.Close() - - // Organize the image URLs by item ID - imageDict := make(map[int][]string) - for rows.Next() { - var img schema.ImageURI - if err := rows.Scan(&img.ItemID, &img.ImageURL); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan images"}) - return - } - imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) - } - - response := make([]map[string]any, 0, len(items)) - - for _, item := range items { - images := imageDict[item.ID] - if images == nil { - images = []string{} - } - - itemData := map[string]any{ - "id": item.ID, - "name": item.ItemName, - "images": images, - } - response = append(response, itemData) - } - - // Always return an array (empty if no items found) - c.JSON(http.StatusOK, response) + GetAllItemsGenericHandler(c, "found", "found_images") } func GetFoundItemByIdHandler(c *gin.Context) { - // Fetch the item by its ID - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"}) - return - } - item, err := found.GetParticularFoundItem(c, id) - if err != nil { - fmt.Println(err.Error()) - c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"}) - return - } - - // Fetch the image URLs associated with the item - var imageURLs []string - rows, err := config.DB.Query(c, "SELECT image_url FROM found_images WHERE item_id = $1", id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - defer rows.Close() - - // Construct the response - for rows.Next() { - var imageURL string - if err := rows.Scan(&imageURL); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan image URL"}) - return - } - imageURLs = append(imageURLs, imageURL) - } - response := LfResponse{ - ID: item.ID, - ItemName: item.ItemName, - ItemDescription: item.ItemDescription, - UserEmail: item.UserEmail, - UserName: item.UserName, - Images: imageURLs, - username: item.UserName, - } - - c.JSON(http.StatusOK, response) + GetItemByIDGenericHandler(c, "found", "found_images") } func DeleteFoundItemHandler(c *gin.Context) { - // Get the user ID - userID, err := helpers.GetUserID(c) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Get item ID from the request - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) - return - } - - // Check if the user is authorized to delete the item - res, err := found.AuthorizeEditDeleteItem(c, id, userID) - if err != nil || !res { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - // Get the image URLs associated with the item - imageURLs, err := found.DeleteAllImageURIsFromFound(c, id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - - // Delete the item from the found table - _, err = config.DB.Exec(c, "DELETE FROM found WHERE id = $1", id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete item"}) - return - } - - // Deletes the images uris from the 'found_images' table in the database - _, err = found.DeleteAnItemImagesFromFound(c, id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from database"}) - return - } - - // Delete images from S3 storage - s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) - if err := s3Client.DeleteImages(imageURLs); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from S3"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Item deleted successfully"}) + DeleteItemGenericHandler(c, "found", "found_images") } func EditFoundItemHandler(c *gin.Context) { - // Get the user ID - userID, err := helpers.GetUserID(c) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Check if the user is authorized to edit the item - itemID, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) - return - } - res, err := found.AuthorizeEditDeleteItem(c, itemID, userID) - if err != nil || !res { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - // Parse the form data - var formData map[string]interface{} - if err := json.NewDecoder(c.Request.Body).Decode(&formData); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) - return - } - - if _, err := found.UpdateInFoundTable(c, itemID, formData); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to edit item"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Item updated successfully"}) + EditItemGenericHandler(c, "found") } func SearchFoundItemHandler(c *gin.Context) { - query := c.Query("query") - - // Fetch found items matching the query - foundItems, err := found.SearchLostItemsFromFound(c, query) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch items"}) - return - } - - var itemIDs []int - for _, item := range foundItems { - itemIDs = append(itemIDs, item.ID) - } - - // Fetch image URLs associated with the items - imageRows, err := found.GetSomeImgUrisFromFound(c, itemIDs) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - - // Organize the image URLs by item ID - imageDict := make(map[int][]string) - for _, img := range imageRows { - imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) - } - - // Construct the response - var response []map[string]interface{} - for _, item := range foundItems { - response = append(response, map[string]interface{}{ - "id": item.ID, - "item_name": item.ItemName, - "item_description": item.ItemDescription, - "user_id": item.UserID, - "created_at": item.CreatedAt, - "images": imageDict[item.ID], // Add the images for this item - }) - } - c.JSON(http.StatusOK, response) + SearchItemsGenericHandler(c, "found", "found_images") } diff --git a/backend-go/internal/controller/lost.go b/backend-go/internal/controller/lost.go index 542625c..b5fddfc 100644 --- a/backend-go/internal/controller/lost.go +++ b/backend-go/internal/controller/lost.go @@ -1,253 +1,33 @@ package controller import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strconv" - - "github.com/LambdaIITH/Dashboard/backend/config" - lost "github.com/LambdaIITH/Dashboard/backend/internal/db" - helpers "github.com/LambdaIITH/Dashboard/backend/internal/helpers" - schema "github.com/LambdaIITH/Dashboard/backend/internal/schema" - "github.com/gin-gonic/gin" ) -type LfResponse struct { - ID int `json:"id"` - ItemName string `json:"item_name"` - ItemDescription string `json:"item_description"` - UserName string `json:"username"` - UserEmail string `json:"user_email"` - Images []string `json:"image_urls"` - CreatedAt string `json:"created_at"` - username string - user_email string -} - /* AddItemHandler handles the addition of a new lost item. It uploads the images to S3 and saves the image URLs in the database. */ func AddItemHandler(c *gin.Context) { - // Step 1: Parse the form data - formData := c.PostForm("form_data") - var formDataDict map[string]interface{} - if err := json.Unmarshal([]byte(formData), &formDataDict); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) - return - } - - // Step 2: Get the user ID - userId, err := helpers.GetUserID(c) - if err != nil { - fmt.Println("ERROR IS HERE", err.Error()) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) - return - } - // Step 3: Insert the form data into the lost table - currItem := schema.LostItem{} - - currItem.ID, err = lost.InsertInLostTable(c, formDataDict, userId) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert data"}) - return - } - - // Step 5: Upload the images to S3 and save the image URLs in the database - form, err := c.MultipartForm() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file data"}) - return - } - - files := form.File["images"] - if len(files) > 0 { - s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) - - imagePaths, err := s3Client.UploadImages(files, currItem.ID, "lost") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload images"}) - return - } - - err = lost.InsertLostImages(c, imagePaths, currItem.ID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image paths"}) - return - } - } - - // Step 6: Return the response - c.JSON(http.StatusOK, gin.H{"message": "Data inserted successfully"}) + AddItemGenericHandler(c, "lost", "lost_images") } /* GetAllItemsHandler fetches all the lost items from the database and returns them as a JSON response. */ func GetAllItemsHandler(c *gin.Context) { - // Step 1: Fetch all the lost items - items, err := lost.GetAllLostItems(c) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch items"}) - return - } - - // Step 2: Fetch the image URLs associated with the items - rows, err := config.DB.Query(c, "SELECT item_id, image_url FROM lost_images") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch images"}) - return - } - defer rows.Close() - - // Step 3: Organize the image URLs by item ID - imageDict := make(map[int][]string) - for rows.Next() { - var img schema.ImageURI - if err := rows.Scan(&img.ItemID, &img.ImageURL); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan images"}) - return - } - imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) - } - - response := make([]map[string]any, 0, len(items)) - - for _, item := range items { - images := imageDict[item.ID] - if images == nil { - images = []string{} - } - - itemData := map[string]any{ - "id": item.ID, - "name": item.ItemName, - "images": images, - } - response = append(response, itemData) - } - - c.JSON(http.StatusOK, response) + GetAllItemsGenericHandler(c, "lost", "lost_images") } /* GetItemByIdHandler fetches a particular lost item by its ID and returns it as a JSON response. */ func GetItemByIdHandler(c *gin.Context) { - // Step 1: Fetch the item by its ID - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"}) - return - } - item, err := lost.GetParticularLostItem(c, id) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"}) - return - } - - // Step 2: Fetch the image URLs associated with the item - var imageURLs []string - rows, err := config.DB.Query(c, "SELECT image_url FROM lost_images WHERE item_id = $1", id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - defer rows.Close() - - // Step 3: Construct the response - for rows.Next() { - var imageURL string - if err := rows.Scan(&imageURL); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan image URL"}) - return - } - imageURLs = append(imageURLs, imageURL) - } - - // Check for errors after iterating through rows - if err = rows.Err(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error iterating through image results"}) - return - } - - // Initialize imageURLs as empty slice if nil - if imageURLs == nil { - imageURLs = []string{} - } - - // Step 4: Return the response - response := LfResponse{ - ID: item.ID, - ItemName: item.ItemName, - ItemDescription: item.ItemDescription, - UserEmail: item.UserEmail, - UserName: item.UserName, - Images: imageURLs, - username: item.UserName, - } - - // Step 5: Return the response - c.JSON(http.StatusOK, response) + GetItemByIDGenericHandler(c, "lost", "lost_images") } func DeleteItemHandler(c *gin.Context) { - // Step 1: Get the user ID - userID, err := helpers.GetUserID(c) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Step 2: Get item ID from the request - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) - return - } - - // Step 3: Check if the user is authorized to delete the item - res, err := lost.AuthorizeEditDeleteItem(c, id, userID) - if err != nil || !res { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - // Step 4: Delete images associated with the item - // Get the image URLs associated with the item - imageURLs, err := lost.DeleteAllImageUrisLost(c, id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - - // Step 5: Delete the item from the lost table - _, err = config.DB.Exec(c, "DELETE FROM lost WHERE id = $1", id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete item"}) - return - } - - // Step 6: Delete the images from the database - // This step deletes the item images from the 'lost_images' table in the database - _, err = lost.DeleteItemImagesFromLost(c, id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from database"}) - return - } - - // Step 7: Delete images from S3 storage - s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) - if err := s3Client.DeleteImages(imageURLs); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from S3"}) - return - } - - // Step 8: Send a success response - c.JSON(http.StatusOK, gin.H{"message": "Item deleted successfully"}) + DeleteItemGenericHandler(c, "lost", "lost_images") } /* @@ -255,89 +35,12 @@ EditItemHandler handles the editing of a lost item. It edits the images in S3 and updates the image URLs in the database. */ func EditItemHandler(c *gin.Context) { - // Step 1: Get the user ID - userID, err := helpers.GetUserID(c) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - // Step 2: Check if the user is authorized to edit the item - itemID, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) - return - } - - res, err := lost.AuthorizeEditDeleteItem(c, itemID, userID) - if err != nil { - return - } - - if !res { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - // Step 3: Parse the form data - var formData map[string]interface{} - if err := json.NewDecoder(c.Request.Body).Decode(&formData); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) - return - } - - if _, err := lost.UpdateInLostTable(c, itemID, formData); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to edit item"}) - return - } - - // Step 4: Upload the images to S3 and save the image URLs in the database - c.JSON(http.StatusOK, gin.H{"message": "Item updated successfully"}) + EditItemGenericHandler(c, "lost") } /* SearchItemHandler fetches the lost items matching the query and returns them as a JSON response. */ func SearchItemHandler(c *gin.Context) { - query := c.Query("query") - - // Step 1: Fetch lost items matching the query - lostItems, err := lost.SearchLostItemsLost(c, query) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch items"}) - return - } - - var itemIDs []int - for _, item := range lostItems { - itemIDs = append(itemIDs, item.ID) - } - - // Step 2: Fetch image URLs associated with the items - imageRows, err := lost.GetSomeImgUrisLost(c, itemIDs) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) - return - } - - // Step 3: Organize the image URLs by item ID - imageDict := make(map[int][]string) - for _, img := range imageRows { - imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) - } - - // Step 4: Construct the response - var response []map[string]interface{} - for _, item := range lostItems { - response = append(response, map[string]interface{}{ - "id": item.ID, - "item_name": item.ItemName, - "item_description": item.ItemDescription, - "user_id": item.UserID, - "created_at": item.CreatedAt, - "images": imageDict[item.ID], // Add the images for this item - }) - } - - // Step 5: Return the response with item details and images - c.JSON(http.StatusOK, response) + SearchItemsGenericHandler(c, "lost", "lost_images") } diff --git a/backend-go/internal/controller/lostandfound_generic.go b/backend-go/internal/controller/lostandfound_generic.go new file mode 100644 index 0000000..ea42627 --- /dev/null +++ b/backend-go/internal/controller/lostandfound_generic.go @@ -0,0 +1,391 @@ +// Package controller provides generic HTTP handlers for Lost and Found items +// This module eliminates code duplication between lost.go and found.go controllers +package controller + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" + + "github.com/LambdaIITH/Dashboard/backend/config" + "github.com/LambdaIITH/Dashboard/backend/internal/db" + "github.com/LambdaIITH/Dashboard/backend/internal/helpers" + "github.com/LambdaIITH/Dashboard/backend/internal/schema" + "github.com/gin-gonic/gin" +) + +// LfResponse represents the response structure for Lost and Found items +type LfResponse struct { + ID int `json:"id"` + ItemName string `json:"item_name"` + ItemDescription string `json:"item_description"` + UserName string `json:"username"` + UserEmail string `json:"user_email"` + Images []string `json:"image_urls"` + CreatedAt string `json:"created_at"` + username string + user_email string +} + +// Whitelist of allowed table names to prevent SQL injection +var allowedControllerTables = map[string]bool{ + "lost": true, + "found": true, + "lost_images": true, + "found_images": true, +} + +// validateTableName checks if a table name is in the allowed list +func validateControllerTableName(tableName string) error { + if !allowedControllerTables[tableName] { + return fmt.Errorf("invalid table name: %s", tableName) + } + return nil +} + +// AddItemGenericHandler handles adding a new item (lost or found) +func AddItemGenericHandler(c *gin.Context, tableName, imagesTableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateControllerTableName(imagesTableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Parse form data + formData := c.PostForm("form_data") + var formDataDict map[string]interface{} + if err := json.Unmarshal([]byte(formData), &formDataDict); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) + return + } + + // Get user ID + userID, err := helpers.GetUserID(c) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + // Insert item into table + itemID, err := db.InsertInTable(c, tableName, formDataDict, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert data"}) + return + } + + // Upload images if provided + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file data"}) + return + } + + files := form.File["images"] + if len(files) > 0 { + s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) + + imagePaths, err := s3Client.UploadImages(files, itemID, tableName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload images"}) + return + } + + err = db.InsertImages(c, imagesTableName, imagePaths, itemID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image paths"}) + return + } + } + + c.JSON(http.StatusOK, gin.H{"message": "Data inserted successfully"}) +} + +// GetAllItemsGenericHandler fetches all items with their images +func GetAllItemsGenericHandler(c *gin.Context, tableName, imagesTableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateControllerTableName(imagesTableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Fetch all items + items, err := db.GetAllItems(c, tableName, imagesTableName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch items"}) + return + } + + // Fetch image URLs + rows, err := config.DB.Query(c, "SELECT item_id, image_url FROM "+imagesTableName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch images"}) + return + } + defer rows.Close() + + // Organize images by item ID + imageDict := make(map[int][]string) + for rows.Next() { + var img schema.ImageURI + if err := rows.Scan(&img.ItemID, &img.ImageURL); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan images"}) + return + } + imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) + } + + // Build response + response := make([]map[string]any, 0, len(items)) + for _, item := range items { + itemID := item["id"].(int) + images := imageDict[itemID] + if images == nil { + images = []string{} + } + + itemData := map[string]any{ + "id": itemID, + "name": item["item_name"], + "images": images, + } + response = append(response, itemData) + } + + c.JSON(http.StatusOK, response) +} + +// GetItemByIDGenericHandler fetches a specific item by ID +func GetItemByIDGenericHandler(c *gin.Context, tableName, imagesTableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateControllerTableName(imagesTableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Parse item ID + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"}) + return + } + + // Get item details + item, err := db.GetParticularItem(c, tableName, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"}) + return + } + + // Fetch image URLs + var imageURLs []string + rows, err := config.DB.Query(c, "SELECT image_url FROM "+imagesTableName+" WHERE item_id = $1", id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) + return + } + defer rows.Close() + + for rows.Next() { + var imageURL string + if err := rows.Scan(&imageURL); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan image URL"}) + return + } + imageURLs = append(imageURLs, imageURL) + } + + if err = rows.Err(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error iterating through image results"}) + return + } + + if imageURLs == nil { + imageURLs = []string{} + } + + // Build response + response := LfResponse{ + ID: item.ID, + ItemName: item.ItemName, + ItemDescription: item.ItemDescription, + UserEmail: item.UserEmail, + UserName: item.UserName, + Images: imageURLs, + username: item.UserName, + } + + c.JSON(http.StatusOK, response) +} + +// DeleteItemGenericHandler deletes an item +func DeleteItemGenericHandler(c *gin.Context, tableName, imagesTableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateControllerTableName(imagesTableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get user ID + userID, err := helpers.GetUserID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get item ID + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) + return + } + + // Check authorization + res, err := db.AuthorizeEditDeleteItem(c, id, userID) + if err != nil || !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + // Get image URLs for deletion from S3 + imageURLs, err := db.DeleteAllImageURIs(c, imagesTableName, id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) + return + } + + // Delete item from main table + _, err = config.DB.Exec(c, "DELETE FROM "+tableName+" WHERE id = $1", id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete item"}) + return + } + + // Delete images from database + _, err = db.DeleteItemImages(c, imagesTableName, id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from database"}) + return + } + + // Delete images from S3 + s3Client := helpers.NewS3Client(os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), os.Getenv("RESOURCE_URI")) + if err := s3Client.DeleteImages(imageURLs); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete images from S3"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Item deleted successfully"}) +} + +// EditItemGenericHandler edits an existing item +func EditItemGenericHandler(c *gin.Context, tableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get user ID + userID, err := helpers.GetUserID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get item ID + itemID, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item id"}) + return + } + + // Check authorization + res, err := db.AuthorizeEditDeleteItem(c, itemID, userID) + if err != nil || !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + // Parse form data + var formData map[string]interface{} + if err := json.NewDecoder(c.Request.Body).Decode(&formData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid form data"}) + return + } + + // Update item + if _, err := db.UpdateInTable(c, tableName, itemID, formData); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to edit item"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Item updated successfully"}) +} + +// SearchItemsGenericHandler searches for items +func SearchItemsGenericHandler(c *gin.Context, tableName, imagesTableName string) { + if err := validateControllerTableName(tableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateControllerTableName(imagesTableName); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + query := c.Query("query") + + // Search items + items, err := db.SearchItems(c, tableName, query) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch items"}) + return + } + + var itemIDs []int + for _, item := range items { + itemIDs = append(itemIDs, item["id"].(int)) + } + + // Fetch image URLs + imageRows, err := db.GetSomeImageURIs(c, imagesTableName, itemIDs) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch images"}) + return + } + + // Organize images by item ID + imageDict := make(map[int][]string) + for _, img := range imageRows { + imageDict[img.ItemID] = append(imageDict[img.ItemID], img.ImageURL) + } + + // Build response + var response []map[string]interface{} + for _, item := range items { + itemID := item["id"].(int) + response = append(response, map[string]interface{}{ + "id": itemID, + "item_name": item["item_name"], + "item_description": item["item_description"], + "user_id": item["user_id"], + "created_at": item["created_at"], + "images": imageDict[itemID], + }) + } + + c.JSON(http.StatusOK, response) +} diff --git a/backend-go/internal/db/lostandfound_generic.go b/backend-go/internal/db/lostandfound_generic.go new file mode 100644 index 0000000..005ce27 --- /dev/null +++ b/backend-go/internal/db/lostandfound_generic.go @@ -0,0 +1,327 @@ +// Package db provides generic database functions for Lost and Found items +// This module eliminates code duplication between lost.go and found.go +package db + +import ( + "context" + "fmt" + + "github.com/LambdaIITH/Dashboard/backend/config" + "github.com/LambdaIITH/Dashboard/backend/internal/schema" +) + +// Whitelist of allowed table names to prevent SQL injection +var allowedTables = map[string]bool{ + "lost": true, + "found": true, + "lost_images": true, + "found_images": true, +} + +// validateTableName checks if a table name is in the allowed list +func validateTableName(tableName string) error { + if !allowedTables[tableName] { + return fmt.Errorf("invalid table name: %s", tableName) + } + return nil +} + +// InsertInTable inserts a new item (lost or found) into the specified table +func InsertInTable(ctx context.Context, tableName string, formData map[string]interface{}, userID int) (int, error) { + if err := validateTableName(tableName); err != nil { + return 0, err + } + + query := fmt.Sprintf(` + INSERT INTO %s (item_name, item_description, user_id) + VALUES ($1, $2, $3) + RETURNING id + `, tableName) + + var itemID int + err := config.DB.QueryRow(ctx, query, formData["item_name"], formData["item_description"], userID).Scan(&itemID) + if err != nil { + return 0, err + } + + return itemID, nil +} + +// InsertImages inserts image URLs for an item into the specified images table +func InsertImages(ctx context.Context, imagesTableName string, imagePaths []string, postID int) error { + if err := validateTableName(imagesTableName); err != nil { + return err + } + + query := fmt.Sprintf(`INSERT INTO %s (image_url, item_id) VALUES ($1, $2)`, imagesTableName) + + for _, imagePath := range imagePaths { + _, err := config.DB.Exec(ctx, query, imagePath, postID) + if err != nil { + return err + } + } + + return nil +} + +// GetAllItems retrieves all items from the specified table with their images +func GetAllItems(ctx context.Context, tableName, imagesTableName string) ([]map[string]interface{}, error) { + if err := validateTableName(tableName); err != nil { + return nil, err + } + if err := validateTableName(imagesTableName); err != nil { + return nil, err + } + + query := fmt.Sprintf(` + SELECT + f.id, + f.item_name, + f.item_description, + f.user_id, + COALESCE(json_agg(fi.image_url) FILTER (WHERE fi.image_url IS NOT NULL), '[]') AS images, + f.created_at + FROM + %s f + LEFT JOIN + %s fi ON f.id = fi.item_id + GROUP BY + f.id, f.item_name, f.item_description, f.user_id, f.created_at + ORDER BY + f.created_at DESC; + `, tableName, imagesTableName) + + rows, err := config.DB.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var items []map[string]interface{} + + for rows.Next() { + var ( + id int + itemName string + itemDescription string + userID int + images []string + createdAt string + ) + + err := rows.Scan(&id, &itemName, &itemDescription, &userID, &images, &createdAt) + if err != nil { + return nil, err + } + + item := map[string]interface{}{ + "id": id, + "item_name": itemName, + "item_description": itemDescription, + "user_id": userID, + "images": images, + "created_at": createdAt, + } + items = append(items, item) + } + + return items, nil +} + +// GetParticularItem retrieves a specific item with user details +func GetParticularItem(ctx context.Context, tableName string, itemID int) (*schema.LostFoundItem, error) { + if err := validateTableName(tableName); err != nil { + return nil, err + } + + query := fmt.Sprintf(` + SELECT + %s.id, + %s.item_name, + %s.item_description, + %s.user_id, + users.name, + users.email, + %s.created_at + FROM %s + INNER JOIN users ON users.id = %s.user_id + WHERE %s.id = $1 + `, tableName, tableName, tableName, tableName, tableName, tableName, tableName, tableName) + + var item schema.LostFoundItem + err := config.DB.QueryRow(ctx, query, itemID).Scan( + &item.ID, + &item.ItemName, + &item.ItemDescription, + &item.UserID, + &item.UserName, + &item.UserEmail, + &item.CreatedAt, + ) + + if err != nil { + return nil, err + } + + return &item, nil +} + +// UpdateInTable updates an item in the specified table +func UpdateInTable(ctx context.Context, tableName string, itemID int, formData map[string]interface{}) (map[string]interface{}, error) { + if err := validateTableName(tableName); err != nil { + return nil, err + } + + query := fmt.Sprintf(` + UPDATE %s + SET item_name = $1, item_description = $2 + WHERE id = $3 + RETURNING id, item_name, item_description, user_id, created_at + `, tableName) + + var ( + id int + itemName string + itemDescription string + userID int + createdAt string + ) + + err := config.DB.QueryRow(ctx, query, formData["item_name"], formData["item_description"], itemID).Scan( + &id, &itemName, &itemDescription, &userID, &createdAt, + ) + + if err != nil { + return nil, err + } + + result := map[string]interface{}{ + "id": id, + "item_name": itemName, + "item_description": itemDescription, + "user_id": userID, + "created_at": createdAt, + } + + return result, nil +} + +// DeleteAllImageURIs deletes and returns all image URLs for an item +func DeleteAllImageURIs(ctx context.Context, imagesTableName string, itemID int) ([]string, error) { + if err := validateTableName(imagesTableName); err != nil { + return nil, err + } + + query := fmt.Sprintf(`SELECT image_url FROM %s WHERE item_id = $1`, imagesTableName) + rows, err := config.DB.Query(ctx, query, itemID) + if err != nil { + return nil, err + } + defer rows.Close() + + var imageURLs []string + for rows.Next() { + var imageURL string + if err := rows.Scan(&imageURL); err != nil { + return nil, err + } + imageURLs = append(imageURLs, imageURL) + } + + return imageURLs, nil +} + +// DeleteItemImages deletes all images for an item from the images table +func DeleteItemImages(ctx context.Context, imagesTableName string, itemID int) (int64, error) { + if err := validateTableName(imagesTableName); err != nil { + return 0, err + } + + query := fmt.Sprintf(`DELETE FROM %s WHERE item_id = $1`, imagesTableName) + result, err := config.DB.Exec(ctx, query, itemID) + if err != nil { + return 0, err + } + + return result.RowsAffected(), nil +} + +// SearchItems searches for items matching the query string +func SearchItems(ctx context.Context, tableName, searchQuery string) ([]map[string]interface{}, error) { + if err := validateTableName(tableName); err != nil { + return nil, err + } + + query := fmt.Sprintf(` + SELECT * + FROM %s + WHERE item_name ILIKE $1 OR item_description ILIKE $1 + ORDER BY created_at DESC + `, tableName) + + searchPattern := "%" + searchQuery + "%" + rows, err := config.DB.Query(ctx, query, searchPattern) + if err != nil { + return nil, err + } + defer rows.Close() + + var items []map[string]interface{} + + for rows.Next() { + var ( + id int + itemName string + itemDescription string + userID int + createdAt string + ) + + err := rows.Scan(&id, &itemName, &itemDescription, &userID, &createdAt) + if err != nil { + return nil, err + } + + item := map[string]interface{}{ + "id": id, + "item_name": itemName, + "item_description": itemDescription, + "user_id": userID, + "created_at": createdAt, + } + items = append(items, item) + } + + return items, nil +} + +// GetSomeImageURIs retrieves image URLs for multiple items +func GetSomeImageURIs(ctx context.Context, imagesTableName string, itemIDs []int) ([]schema.ImageURI, error) { + if err := validateTableName(imagesTableName); err != nil { + return nil, err + } + + if len(itemIDs) == 0 { + return []schema.ImageURI{}, nil + } + + query := fmt.Sprintf(`SELECT item_id, image_url FROM %s WHERE item_id = ANY($1)`, imagesTableName) + + rows, err := config.DB.Query(ctx, query, itemIDs) + if err != nil { + return nil, err + } + defer rows.Close() + + var imageURIs []schema.ImageURI + for rows.Next() { + var img schema.ImageURI + if err := rows.Scan(&img.ItemID, &img.ImageURL); err != nil { + return nil, err + } + imageURIs = append(imageURIs, img) + } + + return imageURIs, nil +} diff --git a/backend-go/internal/schema/lostandfound.go b/backend-go/internal/schema/lostandfound.go index 91452b5..ea406f1 100644 --- a/backend-go/internal/schema/lostandfound.go +++ b/backend-go/internal/schema/lostandfound.go @@ -48,3 +48,14 @@ type ImageURI struct { ItemID int `db:"item_id"` ImageURL string `db:"image_url"` } + +// LostFoundItem is a generic type that can represent both Lost and Found items +type LostFoundItem struct { + ID int `db:"id"` + ItemName string `db:"item_name"` + ItemDescription string `db:"item_description"` + UserID int `db:"user_id"` + UserName string `db:"username"` + UserEmail string `db:"user_email"` + CreatedAt string `db:"created_at"` +} diff --git a/backend/Routes/Lost_and_Found/found.py b/backend/Routes/Lost_and_Found/found.py index 6177183..439c11a 100644 --- a/backend/Routes/Lost_and_Found/found.py +++ b/backend/Routes/Lost_and_Found/found.py @@ -1,154 +1,64 @@ -import os, shutil -import json -from fastapi import APIRouter, HTTPException, Request, status, UploadFile, File, Form -from typing import Dict, Any, List, Tuple -from Routes.Auth.cookie import get_user_id -from utils import * -from models import LfItem, LfResponse +from fastapi import APIRouter, Request, UploadFile, File, Form +from typing import Dict, Any, List from queries.found import * -from utils import S3Client -from .funcs import get_image_dict, authorize_edit_delete +from models import LfResponse +from .generic_handlers import ( + add_item_handler, + get_all_items_handler, + get_item_by_id_handler, + delete_item_handler, + edit_item_handler, + search_items_handler +) router = APIRouter(prefix="/found", tags=["found"]) - -# add found item +# add found item @router.post("/add_item") -async def add_item( request: Request, - form_data: str = Form(...), - images: List[UploadFile] = File(default = None) - ) -> Dict[str, Any]: - - try: - form_data_dict = json.loads(form_data) - user_id = get_user_id(request) - with conn.cursor() as cur: - cur.execute( insert_in_found_table( form_data_dict, user_id ) ) - item = LfItem.from_row(cur.fetchone()) - - # update in elasticsearch - - if images is not None: - image_paths = S3Client.uploadToCloud(images, item.id, "found") - - with conn.cursor() as cur: - cur.execute(insert_found_images(image_paths, item.id)) - conn.commit() - - return {"message": "Data inserted successfully"} - - - except Exception as e: - conn.rollback() - error_message = f"An error occurred: {e}" - print(error_message) - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_message) - - -# show all found item names +async def add_item( + request: Request, + form_data: str = Form(...), + images: List[UploadFile] = File(default=None) +) -> Dict[str, Any]: + return await add_item_handler( + request, + form_data, + images, + "found", + insert_in_found_table, + insert_found_images + ) + + +# show all found item names @router.get("/all") async def get_all_found_item_names() -> List[Dict[str, Any]]: - try: - with conn.cursor() as cur: - cur.execute("SELECT id,item_name FROM found ORDER BY created_at DESC") - rows = cur.fetchall() - - cur.execute("SELECT item_id, image_url from found_images") - images = cur.fetchall() - image_dict = get_image_dict(images) - - rows = list(map(lambda x: {"id": x[0], "name": x[1], "images": image_dict.get(x[0], [])}, rows)) - return rows - - except Exception as e: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed to fetch items") - + return await get_all_items_handler("found", "found_images") + -# show found item with images +# show found item with images @router.get("/item/{id}") def show_found_items(id: int) -> LfResponse: - try: - with conn.cursor() as cur: - cur.execute(get_particular_found_item(id)) - found_item = cur.fetchall() - - if found_item == []: - raise HTTPException(status_code=404, detail="Item not found") - found_item = found_item[0] - cur.execute(get_all_image_uris(id)) - found_images = cur.fetchall() - image_urls = list(map(lambda x: x[0], found_images)) - res = LfResponse.from_row(found_item, image_urls) - return res - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to fetch items: {e}") + return get_item_by_id_handler(id, get_particular_found_item, get_all_image_uris) -# delete a found item -@router.delete( "/delete_item" ) -def delete_found_item( request: Request, item_id: int = Form(...)) -> Dict[str, str]: - user_id = get_user_id(request) - try: - authorize_edit_delete("found", item_id, user_id, conn) - except Exception as e: - raise HTTPException( status_code=500, detail= f"Error: {e}" ) - - try: - with conn.cursor() as cur: - query = f"DELETE from found WHERE found.id = {item_id}" - cur.execute( query ) - S3Client.deleteFromCloud( item_id, "found" ) - conn.commit() - return {"message": "Item deleted successfully!"} - - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail="Failed to fetch items") +# delete a found item +@router.delete("/delete_item") +def delete_found_item(request: Request, item_id: int = Form(...)) -> Dict[str, str]: + return delete_item_handler(request, item_id, "found") -# Update a found item -@router.put( "/edit_item" ) -def edit_selected_item( request: Request, item_id: int = Form(...), form_data:str = Form(...) ) -> Dict[str, str]: - # checking authorization - user_id = get_user_id(request) - try: - authorize_edit_delete("found", item_id, user_id, conn) - except Exception as e: - raise HTTPException( status_code=500, detail= f"Error: {e}" ) - - - # updating - try: - form_data_dict = json.loads(form_data) +# Update a found item +@router.put("/edit_item") +def edit_selected_item( + request: Request, + item_id: int = Form(...), + form_data: str = Form(...) +) -> Dict[str, str]: + return edit_item_handler(request, item_id, form_data, "found", update_in_found_table) - with conn.cursor() as cur: - cur.execute( update_in_found_table( item_id, form_data_dict ) ) - row = cur.fetchone() - item = LfItem.from_row(row) - conn.commit() - - return {"message": "item updated"} - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail=f"Error: {e}") -@router.get("/search") -def search(query : str, max_results: int = 100) -> List[Dict[str, Any]]: - try: - with conn.cursor() as cur: - cur.execute( search_found_items( query, max_results ) ) - res = cur.fetchall() - if len(res) == 0: - return [] - cur.execute(get_some_image_uris([x[0] for x in res])) - images = cur.fetchall() - - image_dict = get_image_dict(images) - res = list( map( lambda x: {"id": x[0], "name": x[1], "images": image_dict.get(x[0], [])}, res ) ) - - - return res - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error: {e}") +@router.get("/search") +def search(query: str, max_results: int = 100) -> List[Dict[str, Any]]: + return search_items_handler(query, max_results, search_found_items, get_some_image_uris) diff --git a/backend/Routes/Lost_and_Found/funcs.py b/backend/Routes/Lost_and_Found/funcs.py index e2b128f..4ed11ee 100644 --- a/backend/Routes/Lost_and_Found/funcs.py +++ b/backend/Routes/Lost_and_Found/funcs.py @@ -11,8 +11,15 @@ def get_image_dict(images: List[Tuple[int, str]]) -> Dict[int, List[str]]: return image_dict def authorize_edit_delete(table: str, item_id: int, user_id:int, conn): + # Validate table name to prevent SQL injection + allowed_tables = {'lost', 'found'} + if table not in allowed_tables: + raise HTTPException(status_code=400, detail="Invalid table name") + with conn.cursor() as cur: - cur.execute( f"SELECT {table}.user_id FROM {table} WHERE {table}.id = {item_id}" ) + # Use parameterized query for item_id but table name is validated + query = f"SELECT {table}.user_id FROM {table} WHERE {table}.id = %s" + cur.execute(query, (item_id,)) authorized_id = cur.fetchone() if not authorized_id: diff --git a/backend/Routes/Lost_and_Found/generic_handlers.py b/backend/Routes/Lost_and_Found/generic_handlers.py new file mode 100644 index 0000000..9b7ac77 --- /dev/null +++ b/backend/Routes/Lost_and_Found/generic_handlers.py @@ -0,0 +1,299 @@ +""" +Generic route handlers for Lost and Found items. +This module provides reusable handler functions that work for both 'lost' and 'found' endpoints. +""" + +import json +from fastapi import HTTPException, Request, status, UploadFile, File, Form +from typing import Dict, Any, List, Callable +from Routes.Auth.cookie import get_user_id +from utils import conn, S3Client +from models import LfItem, LfResponse +from .funcs import get_image_dict, authorize_edit_delete + +# Whitelist of allowed table names to prevent SQL injection +ALLOWED_TABLES = {'lost', 'found'} +ALLOWED_IMAGES_TABLES = {'lost_images', 'found_images'} + + +def _validate_table_name(table_name: str, allowed_tables: set) -> None: + """ + Validate that a table name is in the allowed list. + + Args: + table_name: Name of the table to validate + allowed_tables: Set of allowed table names + + Raises: + HTTPException: If table name is not in allowed list + """ + if table_name not in allowed_tables: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid table name: {table_name}" + ) + + +async def add_item_handler( + request: Request, + form_data: str, + images: List[UploadFile], + table_name: str, + insert_fn: Callable, + insert_images_fn: Callable +) -> Dict[str, Any]: + """ + Generic handler for adding a lost or found item. + + Args: + request: FastAPI request object + form_data: JSON string containing item data + images: List of uploaded images + table_name: Name of the table ('lost' or 'found') + insert_fn: Function to insert item into table + insert_images_fn: Function to insert images + + Returns: + Success message dictionary + """ + _validate_table_name(table_name, ALLOWED_TABLES) + try: + form_data_dict = json.loads(form_data) + user_id = get_user_id(request) + with conn.cursor() as cur: + cur.execute(insert_fn(form_data_dict, user_id)) + item = LfItem.from_row(cur.fetchone()) + + if images is not None: + image_paths = S3Client.uploadToCloud(images, item.id, table_name) + with conn.cursor() as cur: + cur.execute(insert_images_fn(image_paths, item.id)) + + conn.commit() + return {"message": "Data inserted successfully"} + + except Exception as e: + conn.rollback() + error_message = f"An error occurred: {e}" + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=error_message + ) + + +async def get_all_items_handler( + table_name: str, + images_table_name: str +) -> List[Dict[str, Any]]: + """ + Generic handler for getting all lost or found items. + + Args: + table_name: Name of the main table ('lost' or 'found') + images_table_name: Name of the images table ('lost_images' or 'found_images') + + Returns: + List of items with their images + """ + _validate_table_name(table_name, ALLOWED_TABLES) + _validate_table_name(images_table_name, ALLOWED_IMAGES_TABLES) + try: + with conn.cursor() as cur: + cur.execute( + f"SELECT id, item_name FROM {table_name} ORDER BY created_at DESC" + ) + rows = cur.fetchall() + + cur.execute(f"SELECT item_id, image_url from {images_table_name}") + images = cur.fetchall() + + image_dict = get_image_dict(images) + rows = list(map( + lambda x: { + "id": x[0], + "name": x[1], + "images": image_dict.get(x[0], []) + }, + rows + )) + return rows + + except Exception as e: + conn.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to fetch items" + ) + + +def get_item_by_id_handler( + item_id: int, + get_item_fn: Callable, + get_images_fn: Callable +) -> LfResponse: + """ + Generic handler for getting a specific lost or found item. + + Args: + item_id: ID of the item to retrieve + get_item_fn: Function to get the item details + get_images_fn: Function to get image URLs + + Returns: + Item details with images + """ + try: + with conn.cursor() as cur: + cur.execute(get_item_fn(item_id)) + item = cur.fetchall() + + if item == []: + raise HTTPException(status_code=404, detail="Item not found") + + item = item[0] + cur.execute(get_images_fn(item_id)) + images = cur.fetchall() + image_urls = list(map(lambda x: x[0], images)) + res = LfResponse.from_row(item, image_urls) + return res + + except Exception as e: + conn.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to fetch items: {e}" + ) + + +def delete_item_handler( + request: Request, + item_id: int, + table_name: str +) -> Dict[str, str]: + """ + Generic handler for deleting a lost or found item. + + Args: + request: FastAPI request object + item_id: ID of the item to delete + table_name: Name of the table ('lost' or 'found') + + Returns: + Success message dictionary + """ + _validate_table_name(table_name, ALLOWED_TABLES) + user_id = get_user_id(request) + + try: + authorize_edit_delete(table_name, item_id, user_id, conn) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error: {e}") + + try: + with conn.cursor() as cur: + # Use parameterized query for item_id (table_name is already validated) + query = f"DELETE from {table_name} WHERE {table_name}.id = %s" + cur.execute(query, (item_id,)) + + S3Client.deleteFromCloud(item_id, table_name) + conn.commit() + + return {"message": "Item deleted successfully!"} + + except Exception as e: + conn.rollback() + raise HTTPException( + status_code=500, + detail="Failed to fetch items" + ) + + +def edit_item_handler( + request: Request, + item_id: int, + form_data: str, + table_name: str, + update_fn: Callable +) -> Dict[str, str]: + """ + Generic handler for editing a lost or found item. + + Args: + request: FastAPI request object + item_id: ID of the item to edit + form_data: JSON string containing updated data + table_name: Name of the table ('lost' or 'found') + update_fn: Function to update the item + + Returns: + Success message dictionary + """ + _validate_table_name(table_name, ALLOWED_TABLES) + user_id = get_user_id(request) + + try: + authorize_edit_delete(table_name, item_id, user_id, conn) + except Exception as e: + conn.rollback() + raise HTTPException(status_code=500, detail=f"Error: {e}") + + try: + form_data_dict = json.loads(form_data) + + with conn.cursor() as cur: + cur.execute(update_fn(item_id, form_data_dict)) + row = cur.fetchone() + item = LfItem.from_row(row) + + conn.commit() + return {"message": "item updated"} + + except Exception as e: + conn.rollback() + raise HTTPException(status_code=500, detail=f"Error: {e}") + + +def search_items_handler( + query: str, + max_results: int, + search_fn: Callable, + get_images_fn: Callable +) -> List[Dict[str, Any]]: + """ + Generic handler for searching lost or found items. + + Args: + query: Search query string + max_results: Maximum number of results to return + search_fn: Function to search items + get_images_fn: Function to get images for multiple items + + Returns: + List of matching items with their images + """ + try: + with conn.cursor() as cur: + cur.execute(search_fn(query, max_results)) + res = cur.fetchall() + + if len(res) == 0: + return [] + + cur.execute(get_images_fn([x[0] for x in res])) + images = cur.fetchall() + + image_dict = get_image_dict(images) + res = list(map( + lambda x: { + "id": x[0], + "name": x[1], + "images": image_dict.get(x[0], []) + }, + res + )) + + return res + + except Exception as e: + conn.rollback() + raise HTTPException(status_code=500, detail=f"Error: {e}") diff --git a/backend/Routes/Lost_and_Found/lost.py b/backend/Routes/Lost_and_Found/lost.py index ff1a831..a7303cd 100644 --- a/backend/Routes/Lost_and_Found/lost.py +++ b/backend/Routes/Lost_and_Found/lost.py @@ -1,161 +1,66 @@ -import os, shutil -import json -from fastapi import APIRouter, HTTPException, Request, status, UploadFile, File, Form +from fastapi import APIRouter, Request, UploadFile, File, Form from typing import Dict, Any, List -from Routes.Auth.cookie import get_user_id -from utils import * from queries.lost import * -from models import LfItem, LfResponse -from utils import S3Client -from .funcs import * -router = APIRouter(prefix="/lost", tags=["lost"]) +from models import LfResponse +from .generic_handlers import ( + add_item_handler, + get_all_items_handler, + get_item_by_id_handler, + delete_item_handler, + edit_item_handler, + search_items_handler +) -# add lost item -@router.post("/add_item") -async def add_item( request: Request, - form_data: str = Form(...), - images: List[UploadFile] = File(default = None), - - ) -> Dict[str, Any]: - - try: - form_data_dict = json.loads(form_data) - user_id = get_user_id(request) - with conn.cursor() as cur: - cur.execute( insert_in_lost_table( form_data_dict, user_id ) ) - item = LfItem.from_row(cur.fetchone()) - - # update in elasticsearch - - if images is not None: - image_paths = S3Client.uploadToCloud(images, item.id, "lost") +router = APIRouter(prefix="/lost", tags=["lost"]) - with conn.cursor() as cur: - cur.execute(insert_lost_images(image_paths, item.id)) - conn.commit() - return {"message": "Data inserted successfully"} +# add lost item +@router.post("/add_item") +async def add_item( + request: Request, + form_data: str = Form(...), + images: List[UploadFile] = File(default=None), +) -> Dict[str, Any]: + return await add_item_handler( + request, + form_data, + images, + "lost", + insert_in_lost_table, + insert_lost_images + ) - except Exception as e: - conn.rollback() - error_message = f"An error occurred: {e}" - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_message) -# show all lost item names sorted by created_at +# show all lost item names sorted by created_at @router.get("/all") async def get_all_lost_item_names() -> List[Dict[str, Any]]: - try: - with conn.cursor() as cur: - cur.execute("SELECT id, item_name FROM lost ORDER BY created_at DESC") - rows = cur.fetchall() - cur.execute("SELECT item_id, image_url from lost_images") - images = cur.fetchall() - - image_dict = get_image_dict(images) - rows = list(map(lambda x: {"id": x[0], "name": x[1], "images": image_dict.get(x[0], [])}, rows)) - - return rows - - except Exception as e: - conn.rollback() - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed to fetch items") - + return await get_all_items_handler("lost", "lost_images") -# show some lost items with images + +# show some lost items with images @router.get("/item/{id}") def show_lost_items(id: int) -> LfResponse: - try: - with conn.cursor() as cur: - cur.execute(get_particular_lost_item(id)) - - lost_item = cur.fetchall() - if lost_item == []: - raise HTTPException(status_code=404, detail="Item not found") - lost_item = lost_item[0] - cur.execute(get_all_image_uris(id)) - lost_images = cur.fetchall() - image_urls = list(map(lambda x: x[0], lost_images)) - res = LfResponse.from_row(lost_item, image_urls) - return res - - - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail=f"Failed to fetch items: {e}") - + return get_item_by_id_handler(id, get_particular_lost_item, get_all_image_uris) -# delete a lost item -@router.delete( "/delete_item" ) -def delete_lost_item(request: Request, item_id: int = Form(...) ) -> Dict[str, str]: - user_id = get_user_id(request) - try: - authorize_edit_delete("lost", item_id, user_id, conn) - except Exception as e: - raise HTTPException( status_code=500, detail= f"Error: {e}" ) - - try: - with conn.cursor() as cur: - query = f"DELETE from lost WHERE lost.id = {item_id}" - cur.execute( query ) - - S3Client.deleteFromCloud( item_id, "lost" ) - conn.commit() - - return {"message": "Item deleted successfully!"} - - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail="Failed to fetch items") +# delete a lost item +@router.delete("/delete_item") +def delete_lost_item(request: Request, item_id: int = Form(...)) -> Dict[str, str]: + return delete_item_handler(request, item_id, "lost") -# Update a lost item -@router.put( "/edit_item" ) -def edit_selected_item(request: Request, item_id: int = Form(...), form_data:str = Form(...)) -> Dict[str, str]: - # checking authorization - user_id = get_user_id(request) - try: - authorize_edit_delete("lost", item_id, user_id, conn) - except Exception as e: - conn.rollback() - raise HTTPException( status_code=500, detail= f"Error: {e}" ) - - - # updating - try: - form_data_dict = json.loads(form_data) - with conn.cursor() as cur: - cur.execute( update_in_lost_table( item_id, form_data_dict ) ) - row = cur.fetchone() - item = LfItem.from_row(row) - - conn.commit() - - - return {"message": "item updated"} - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail=f"Error: {e}") +# Update a lost item +@router.put("/edit_item") +def edit_selected_item( + request: Request, + item_id: int = Form(...), + form_data: str = Form(...) +) -> Dict[str, str]: + return edit_item_handler(request, item_id, form_data, "lost", update_in_lost_table) # search lost items @router.get("/search") -def search(query : str, max_results: int = 100) -> List[Dict[str, Any]] : - try: - with conn.cursor() as cur: - cur.execute( search_lost_items( query, max_results ) ) - res = cur.fetchall() - - if len(res) == 0: - return [] - cur.execute(get_some_image_uris([x[0] for x in res])) - images = cur.fetchall() - - image_dict = get_image_dict(images) - res = list( map( lambda x: {"id": x[0], "name": x[1], "images": image_dict.get(x[0], [])}, res ) ) - - return res - except Exception as e: - conn.rollback() - raise HTTPException(status_code=500, detail=f"Error: {e}") +def search(query: str, max_results: int = 100) -> List[Dict[str, Any]]: + return search_items_handler(query, max_results, search_lost_items, get_some_image_uris) diff --git a/backend/queries/found.py b/backend/queries/found.py index 02a4fca..5a8d1ea 100644 --- a/backend/queries/found.py +++ b/backend/queries/found.py @@ -1,89 +1,62 @@ +""" +Found items query module. +This module provides query functions for the 'found' table by wrapping the generic functions. +""" -from pypika import Query, Table, functions as fn, Order from typing import Dict, Any +from .lost_found_generic import ( + insert_in_table as generic_insert_in_table, + insert_images as generic_insert_images, + get_all_items as generic_get_all_items, + update_in_table as generic_update_in_table, + get_particular_item as generic_get_particular_item, + delete_an_item_images as generic_delete_an_item_images, + get_all_image_uris as generic_get_all_image_uris, + search_items as generic_search_items, + get_some_image_uris as generic_get_some_image_uris +) -found_table, found_images_table = Table('found'), Table('found_images') -users = Table("users") -def insert_in_found_table( form_data: Dict[str, Any], user_id: int ): - query = Query.into(found_table).columns('item_name', 'item_description', 'user_id').insert( - form_data['item_name'], form_data['item_description'], user_id - ) - - sql_query = query.get_sql() - sql_query += " RETURNING *" - - return sql_query - - -def insert_found_images( image_paths: list, post_id: int): - - query = Query.into(found_images_table).columns('image_url', 'item_id') - - for image_path in image_paths: - query = query.insert(image_path, post_id) - - return query.get_sql() - - -def get_all_found_items(): - query = """ - SELECT - f.id, - f.item_name, - f.item_description, - f.user_id, - COALESCE(json_agg(fi.image_url) FILTER (WHERE fi.image_url IS NOT NULL), '[]') AS images, - f.created_at - FROM - found f - LEFT JOIN - found_images fi ON f.id = fi.item_id - GROUP BY - f.id, f.item_name, f.item_description, f.user_id - ORDER BY - f.created_at DESC; - """ - - return query - - -def update_in_found_table( item_id: int, form_data: Dict[str, Any] ): - query = Query.update(found_table) - - for key, value in form_data.items(): - if key == 'item_name' or key == 'item_description': - query = query.set(found_table[key], value) - - query = query.where(found_table['id'] == item_id).get_sql() - query += " RETURNING *" - - return query - -def get_particular_found_item(item_id: int): - query = (Query.from_(found_table) - .join(users) - .on(users['id'] == found_table['user_id']) - .select('*') - .where(found_table['id'] == item_id)) - return str(query) - -def delete_an_item_images(item_id: int): - query = Query.from_(found_images_table).delete().where(found_images_table['item_id'] == item_id) - return str(query) - -def get_all_image_uris(item_id : int): - query = Query.from_(found_images_table).select('image_url').where(found_images_table['item_id'] == item_id) - return str(query) - -def search_found_items(search_query: str, max_results: int= 10): - query = (Query.from_(found_table) - .select('*') - .where(found_table['item_name'].ilike(f'%{search_query}%') | found_table['item_description'].ilike(f'%{search_query}%')) - .orderby(found_table['created_at'], order=Order.desc) - .limit(max_results) - ) - return str(query) - -def get_some_image_uris(item_ids : list): - query = Query.from_(found_images_table).select(found_images_table['item_id'], found_images_table['image_url']).where(found_images_table['item_id'].isin(item_ids)) - return str(query) \ No newline at end of file + +def insert_in_found_table(form_data: Dict[str, Any], user_id: int) -> str: + """Insert a new found item.""" + return generic_insert_in_table('found', form_data, user_id) + + +def insert_found_images(image_paths: list, post_id: int) -> str: + """Insert images for a found item.""" + return generic_insert_images('found_images', image_paths, post_id) + + +def get_all_found_items() -> str: + """Get all found items with their images.""" + return generic_get_all_items('found', 'found_images') + + +def update_in_found_table(item_id: int, form_data: Dict[str, Any]) -> str: + """Update a found item.""" + return generic_update_in_table('found', item_id, form_data) + + +def get_particular_found_item(item_id: int) -> str: + """Get a specific found item with user details.""" + return generic_get_particular_item('found', item_id) + + +def delete_an_item_images(item_id: int) -> str: + """Delete all images for a found item.""" + return generic_delete_an_item_images('found_images', item_id) + + +def get_all_image_uris(item_id: int) -> str: + """Get all image URLs for a found item.""" + return generic_get_all_image_uris('found_images', item_id) + + +def search_found_items(search_query: str, max_results: int = 10) -> str: + """Search for found items.""" + return generic_search_items('found', search_query, max_results) + + +def get_some_image_uris(item_ids: list) -> str: + """Get image URLs for multiple found items.""" + return generic_get_some_image_uris('found_images', item_ids) \ No newline at end of file diff --git a/backend/queries/lost.py b/backend/queries/lost.py index 2b2cab2..714cdf3 100644 --- a/backend/queries/lost.py +++ b/backend/queries/lost.py @@ -1,87 +1,62 @@ +""" +Lost items query module. +This module provides query functions for the 'lost' table by wrapping the generic functions. +""" -from pypika import Query, Table, functions as fn, Order from typing import Dict, Any +from .lost_found_generic import ( + insert_in_table as generic_insert_in_table, + insert_images as generic_insert_images, + get_all_items as generic_get_all_items, + update_in_table as generic_update_in_table, + get_particular_item as generic_get_particular_item, + delete_an_item_images as generic_delete_an_item_images, + get_all_image_uris as generic_get_all_image_uris, + search_items as generic_search_items, + get_some_image_uris as generic_get_some_image_uris +) -lost_table, lost_images_table = Table('lost'), Table('lost_images') -users = Table("users") -def insert_in_lost_table( form_data: Dict[str, Any], user_id: int ): - query = Query.into(lost_table).columns('item_name', 'item_description', 'user_id').insert( - form_data['item_name'], form_data['item_description'], user_id - ) - - sql_query = query.get_sql() - sql_query += " RETURNING *" - - return sql_query - - -def insert_lost_images( image_paths: list, post_id: int): - - query = Query.into(lost_images_table).columns('image_url', 'item_id') - - for image_path in image_paths: - query = query.insert(image_path, post_id) - - return query.get_sql() - - -def get_all_lost_items(): - query = """ - SELECT - f.id, - f.item_name, - f.item_description, - f.user_id, - COALESCE(json_agg(fi.image_url) FILTER (WHERE fi.image_url IS NOT NULL), '[]') AS images, - f.created_at - FROM - lost f - LEFT JOIN - lost_images fi ON f.id = fi.item_id - GROUP BY - f.id, f.item_name, f.item_description, f.user_id - ORDER BY - f.created_at DESC; - """ - - return query - -def update_in_lost_table( item_id: int, form_data: Dict[str, Any]): - query = Query.update(lost_table) - - for key, value in form_data.items(): - if key == 'item_name' or key == 'item_description': - query = query.set(lost_table[key], value) - - query = query.where(lost_table['id'] == item_id).get_sql() - query += " RETURNING *" - return query - -def get_particular_lost_item(item_id: int): - query = (Query.from_(lost_table) - .join(users) - .on(users['id'] == lost_table['user_id']) - .select('*') - .where(lost_table['id'] == item_id)) - return str(query) - -def delete_an_item_images(item_id:int): - query = Query.from_(lost_images_table).delete().where(lost_images_table['item_id'] == item_id) - return str(query) - -def get_all_image_uris(item_id: int): - query = Query.from_(lost_images_table).select('image_url').where(lost_images_table['item_id'] == item_id) - return str(query) - -def search_lost_items(search_query: str, max_results: int= 10): - query = (Query.from_(lost_table) - .select('*') - .where(lost_table['item_name'].ilike(f'%{search_query}%') | lost_table['item_description'].ilike(f'%{search_query}%')) - .orderby(lost_table['created_at'], order=Order.desc) - .limit(max_results) - ) - return str(query) - -def get_some_image_uris(item_ids : list): - query = Query.from_(lost_images_table).select(lost_images_table['item_id'], lost_images_table['image_url']).where(lost_images_table['item_id'].isin(item_ids)) - return str(query) \ No newline at end of file + +def insert_in_lost_table(form_data: Dict[str, Any], user_id: int) -> str: + """Insert a new lost item.""" + return generic_insert_in_table('lost', form_data, user_id) + + +def insert_lost_images(image_paths: list, post_id: int) -> str: + """Insert images for a lost item.""" + return generic_insert_images('lost_images', image_paths, post_id) + + +def get_all_lost_items() -> str: + """Get all lost items with their images.""" + return generic_get_all_items('lost', 'lost_images') + + +def update_in_lost_table(item_id: int, form_data: Dict[str, Any]) -> str: + """Update a lost item.""" + return generic_update_in_table('lost', item_id, form_data) + + +def get_particular_lost_item(item_id: int) -> str: + """Get a specific lost item with user details.""" + return generic_get_particular_item('lost', item_id) + + +def delete_an_item_images(item_id: int) -> str: + """Delete all images for a lost item.""" + return generic_delete_an_item_images('lost_images', item_id) + + +def get_all_image_uris(item_id: int) -> str: + """Get all image URLs for a lost item.""" + return generic_get_all_image_uris('lost_images', item_id) + + +def search_lost_items(search_query: str, max_results: int = 10) -> str: + """Search for lost items.""" + return generic_search_items('lost', search_query, max_results) + + +def get_some_image_uris(item_ids: list) -> str: + """Get image URLs for multiple lost items.""" + return generic_get_some_image_uris('lost_images', item_ids) \ No newline at end of file diff --git a/backend/queries/lost_found_generic.py b/backend/queries/lost_found_generic.py new file mode 100644 index 0000000..edb519c --- /dev/null +++ b/backend/queries/lost_found_generic.py @@ -0,0 +1,230 @@ +""" +Generic query builder for Lost and Found items. +This module provides reusable query functions that work for both 'lost' and 'found' tables. +""" + +from pypika import Query, Table, Order +from typing import Dict, Any + +# Whitelist of allowed table names to prevent SQL injection +ALLOWED_TABLES = {'lost', 'found'} +ALLOWED_IMAGES_TABLES = {'lost_images', 'found_images'} + + +def _validate_table_name(table_name: str, allowed_tables: set) -> None: + """ + Validate that a table name is in the allowed list. + + Args: + table_name: Name of the table to validate + allowed_tables: Set of allowed table names + + Raises: + ValueError: If table name is not in allowed list + """ + if table_name not in allowed_tables: + raise ValueError(f"Invalid table name: {table_name}") + + +def insert_in_table(table_name: str, form_data: Dict[str, Any], user_id: int) -> str: + """ + Generic insert query for lost/found items. + + Args: + table_name: Name of the table ('lost' or 'found') + form_data: Dictionary containing item_name and item_description + user_id: ID of the user creating the item + + Returns: + SQL query string with RETURNING clause + """ + _validate_table_name(table_name, ALLOWED_TABLES) + table = Table(table_name) + query = Query.into(table).columns('item_name', 'item_description', 'user_id').insert( + form_data['item_name'], form_data['item_description'], user_id + ) + + sql_query = query.get_sql() + sql_query += " RETURNING *" + + return sql_query + + +def insert_images(table_name: str, image_paths: list, post_id: int) -> str: + """ + Generic insert query for item images. + + Args: + table_name: Name of the images table ('lost_images' or 'found_images') + image_paths: List of image URLs to insert + post_id: ID of the item + + Returns: + SQL query string + """ + _validate_table_name(table_name, ALLOWED_IMAGES_TABLES) + images_table = Table(table_name) + query = Query.into(images_table).columns('image_url', 'item_id') + + for image_path in image_paths: + query = query.insert(image_path, post_id) + + return query.get_sql() + + +def get_all_items(table_name: str, images_table_name: str) -> str: + """ + Generic query to get all items with their images. + + Args: + table_name: Name of the main table ('lost' or 'found') + images_table_name: Name of the images table ('lost_images' or 'found_images') + + Returns: + SQL query string + """ + _validate_table_name(table_name, ALLOWED_TABLES) + _validate_table_name(images_table_name, ALLOWED_IMAGES_TABLES) + query = f""" + SELECT + f.id, + f.item_name, + f.item_description, + f.user_id, + COALESCE(json_agg(fi.image_url) FILTER (WHERE fi.image_url IS NOT NULL), '[]') AS images, + f.created_at + FROM + {table_name} f + LEFT JOIN + {images_table_name} fi ON f.id = fi.item_id + GROUP BY + f.id, f.item_name, f.item_description, f.user_id + ORDER BY + f.created_at DESC; + """ + + return query + + +def update_in_table(table_name: str, item_id: int, form_data: Dict[str, Any]) -> str: + """ + Generic update query for lost/found items. + + Args: + table_name: Name of the table ('lost' or 'found') + item_id: ID of the item to update + form_data: Dictionary containing fields to update + + Returns: + SQL query string with RETURNING clause + """ + _validate_table_name(table_name, ALLOWED_TABLES) + table = Table(table_name) + query = Query.update(table) + + for key, value in form_data.items(): + if key == 'item_name' or key == 'item_description': + query = query.set(table[key], value) + + query = query.where(table['id'] == item_id).get_sql() + query += " RETURNING *" + return query + + +def get_particular_item(table_name: str, item_id: int) -> str: + """ + Generic query to get a specific item with user details. + + Args: + table_name: Name of the table ('lost' or 'found') + item_id: ID of the item + + Returns: + SQL query string + """ + _validate_table_name(table_name, ALLOWED_TABLES) + table = Table(table_name) + users = Table("users") + query = (Query.from_(table) + .join(users) + .on(users['id'] == table['user_id']) + .select('*') + .where(table['id'] == item_id)) + return str(query) + + +def delete_an_item_images(images_table_name: str, item_id: int) -> str: + """ + Generic query to delete all images for an item. + + Args: + images_table_name: Name of the images table ('lost_images' or 'found_images') + item_id: ID of the item + + Returns: + SQL query string + """ + _validate_table_name(images_table_name, ALLOWED_IMAGES_TABLES) + images_table = Table(images_table_name) + query = Query.from_(images_table).delete().where(images_table['item_id'] == item_id) + return str(query) + + +def get_all_image_uris(images_table_name: str, item_id: int) -> str: + """ + Generic query to get all image URLs for an item. + + Args: + images_table_name: Name of the images table ('lost_images' or 'found_images') + item_id: ID of the item + + Returns: + SQL query string + """ + _validate_table_name(images_table_name, ALLOWED_IMAGES_TABLES) + images_table = Table(images_table_name) + query = Query.from_(images_table).select('image_url').where(images_table['item_id'] == item_id) + return str(query) + + +def search_items(table_name: str, search_query: str, max_results: int = 10) -> str: + """ + Generic search query for lost/found items. + + Args: + table_name: Name of the table ('lost' or 'found') + search_query: Search string to match against item_name and item_description + max_results: Maximum number of results to return + + Returns: + SQL query string + """ + _validate_table_name(table_name, ALLOWED_TABLES) + table = Table(table_name) + query = (Query.from_(table) + .select('*') + .where(table['item_name'].ilike(f'%{search_query}%') | table['item_description'].ilike(f'%{search_query}%')) + .orderby(table['created_at'], order=Order.desc) + .limit(max_results) + ) + return str(query) + + +def get_some_image_uris(images_table_name: str, item_ids: list) -> str: + """ + Generic query to get image URLs for multiple items. + + Args: + images_table_name: Name of the images table ('lost_images' or 'found_images') + item_ids: List of item IDs + + Returns: + SQL query string + """ + _validate_table_name(images_table_name, ALLOWED_IMAGES_TABLES) + images_table = Table(images_table_name) + query = Query.from_(images_table).select( + images_table['item_id'], + images_table['image_url'] + ).where(images_table['item_id'].isin(item_ids)) + return str(query)