package server import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "backend/database" "backend/models" "fmt" "log" "mime/multipart" "net/http" "os" "path/filepath" "time" ) // FrontendReviewPayload: EXPECTS `user` AND `place` AS STRINGS type FrontendReviewPayload struct { User string `json:"user"` Place string `json:"place"` Quiet int `json:"quiet"` Clean int `json:"clean"` Private int `json:"private"` CleanWudu int `json:"cleanWudu"` ChildFriendly int `json:"childFriendly"` Safe int `json:"safe"` Notes string `json:"notes"` } // FrontendReview: Type used when marshalling reviews for the frontend (e.g., in QueryReviews) type FrontendReview struct { User string `json:"User"` Place string `json:"Place"` Quiet int `json:"Quiet"` Clean int `json:"Clean"` CleanWudu int `json:"CleanWudu"` Private int `json:"Private"` ChildFriendly int `json:"ChildFriendly"` Safe int `json:"Safe"` Notes string `json:"Notes"` } // FrontendPlacePayload: For adding new places - NOW INCLUDES WebsiteURL type FrontendPlacePayload struct { Name string `json:"name"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Address string `json:"address"` LocationType string `json:"location_type"` WomensSpace bool `json:"womens_space"` Wudu bool `json:"wudu"` Notes string `json:"notes"` WebsiteURL string `json:"website_url"` // ADDED: Matches frontend's 'website' field in AddMosqueScreen } // FrontendPlace: Type used when marshalling places for the frontend (e.g., in QueryPlaces) type FrontendPlace struct { ID uuid.UUID `json:"ID"` Name string `json:"Name"` Latitude float64 `json:"Latitude"` Longitude float64 `json:"Longitude"` Address string `json:"Address"` LocationType string `json:"LocationType"` WomensSpace bool `json:"WomensSpace"` Wudu bool `json:"Wudu"` WebsiteURL string `json:"WebsiteURL"` Notes string `json:"Notes"` Images []models.PlaceImage `json:"Images"` Reviews []models.Review `json:"Reviews"` } func HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, "status: healthy") } func QueryPlaces(c *gin.Context) { var places []models.Place result := database.DB.Find(&places) if result.Error != nil { log.Printf("[ERROR]: error while retrieving places from database: %s", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to retrieve places: %s", result.Error)}) return } var newPlaces []FrontendPlace for _, oldPlace := range places { newPlace := toFrontendPlace(oldPlace) newPlaces = append(newPlaces, newPlace) } c.JSON(http.StatusOK, newPlaces) } func QueryImages(c *gin.Context) { var images []models.PlaceImage result := database.DB.Find(&images) if result.Error != nil { log.Printf("[ERROR]: error while retrieving images from database: %s", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to retrieve images: %s", result.Error)}) return } c.JSON(http.StatusOK, images) } func QueryImagesForPlace(c *gin.Context) { placeID := c.Param("place_id") var images []models.PlaceImage result := database.DB. Where("place_id = ?", placeID). Find(&images) if result.Error != nil { log.Printf("[ERROR]: error while retrieving images for place from database: %s", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to retrieve images for place: %s", result.Error)}) return } c.JSON(http.StatusOK, images) } func QueryReviews(c *gin.Context) { var reviews []models.Review database.DB.Find(&reviews) frontendReviews := []FrontendReview{} for _, oldReview := range reviews { newReview := toFrontendReview(oldReview) frontendReviews = append(frontendReviews, newReview) log.Printf("%+v\n", newReview) } log.Printf("[LOG]: Fetched frontend reviews: %+v", frontendReviews) c.JSON(http.StatusOK, frontendReviews) } func SaveReview(c *gin.Context) { var payload FrontendReviewPayload if err := c.BindJSON(&payload); err != nil { log.Printf("[ERROR]: Failed to bind JSON for review: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) return } log.Printf("[LOG]: Received review payload: %+v", payload) // Look up Place by Name var place models.Place database.DB.Where("name = ?", payload.Place).First(&place) if place.ID == uuid.Nil { log.Printf("[ERROR]: Place with name '%s' not found for review submission.", payload.Place) c.JSON(http.StatusNotFound, gin.H{"error": "Place not found. Please ensure the place name is correct."}) return } // Look up Reviewer by Username var reviewer models.User database.DB.Where("username = ?", payload.User).First(&reviewer) if reviewer.ID == uuid.Nil { log.Printf("[ERROR]: Reviewer with username '%s' not found for review submission.", payload.User) c.JSON(http.StatusNotFound, gin.H{"error": "Reviewer user not found. Please ensure username is correct."}) return } review := models.Review{ ID: uuid.New(), Reviewer: reviewer.ID, Place: place.ID, Quiet: payload.Quiet, Clean: payload.Clean, Private: payload.Private, CleanWudu: payload.CleanWudu, ChildFriendly: payload.ChildFriendly, Safe: payload.Safe, Notes: payload.Notes, } result := database.DB.Create(&review) if result.Error != nil { log.Printf("[ERROR]: Error creating review in DB: %s", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) return } c.JSON(http.StatusCreated, gin.H{"message": "Review submitted successfully!", "reviewId": review.ID.String()}) } // SavePlace function (for adding new mosques) func SavePlace(c *gin.Context) { var payload FrontendPlacePayload if err := c.BindJSON(&payload); err != nil { log.Printf("[ERROR]: Failed to bind JSON for new place: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) return } log.Printf("[LOG]: Received new place payload: %+v", payload) place := models.Place{ ID: uuid.New(), Name: payload.Name, Latitude: payload.Latitude, Longitude: payload.Longitude, Address: payload.Address, LocationType: payload.LocationType, WomensSpace: payload.WomensSpace, Wudu: payload.Wudu, Notes: payload.Notes, WebsiteURL: payload.WebsiteURL, // Ensure models.Place has WebsiteURL field } result := database.DB.Create(&place) if result.Error != nil { log.Printf("[ERROR]: Error creating new place in DB: %s", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) return } c.JSON(http.StatusCreated, gin.H{"message": "Place added successfully!", "placeId": place.ID.String()}) } // UploadImage function (Fixed ImageURL storage) func UploadImage(c *gin.Context) { placeIDStr := c.PostForm("place_id") notes := c.PostForm("Notes") if placeIDStr == "" { log.Printf("[ERROR]: Place ID is missing in image upload.") c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Place ID is required for image upload."}) return } file, err := c.FormFile("image") if err != nil { log.Printf("[ERROR]: Failed to get image file from form: %s", err) c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": fmt.Sprintf("Failed to get image file: %s", err)}) return } if !isValidImage(file) { log.Printf("[ERROR]: Invalid image file type for upload: %s", file.Header.Get("Content-Type")) c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid file type. Only JPEG, PNG, and GIF are allowed."}) return } placeUUID, err := uuid.Parse(placeIDStr) if err != nil { log.Printf("[ERROR]: Invalid Place ID UUID format for image upload '%s': %s", placeIDStr, err) c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid Place ID format."}) return } var existingPlace models.Place if err := database.DB.First(&existingPlace, placeUUID).Error; err != nil { log.Printf("[ERROR]: Place with ID %s not found for image upload: %s", placeUUID, err) c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "Place not found."}) return } // Generate a unique filename and path ext := filepath.Ext(file.Filename) uniqueFileName := fmt.Sprintf("%s-%s%s", time.Now().Format("20060102-150405"), uuid.New().String(), ext) // filePath is where the file is physically saved on the server filePath := filepath.Join("uploads", uniqueFileName) // dbImageURL is the path stored in the database, relative to the web server's static route dbImageURL := fmt.Sprintf("/uploads/%s", uniqueFileName) // Correct format for web serving if err := c.SaveUploadedFile(file, filePath); err != nil { log.Printf("[ERROR]: Failed to save uploaded file '%s' to %s: %s", file.Filename, filePath, err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to save image file on server."}) return } // Store image metadata in the database (place_images table) image := models.PlaceImage{ ID: uuid.New(), PlaceID: placeUUID, ImageURL: dbImageURL, // FIXED: Store the web-accessible relative URL here Notes: notes, // Store notes from the form } result := database.DB.Create(&image) if result.Error != nil { log.Printf("[ERROR]: Error saving image metadata to database: %s", result.Error) // Clean up the file if DB save fails if removeErr := os.Remove(filePath); removeErr != nil { log.Printf("[ERROR]: Failed to clean up uploaded file '%s' after DB error: %s", filePath, removeErr) } c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": fmt.Sprintf("Failed to save image metadata: %s", result.Error)}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "File uploaded successfully", "imageUrl": dbImageURL, // Return the relative path "imageId": image.ID.String(), "placeId": image.PlaceID.String(), }) } func UpdatePlaceWebsite(c *gin.Context) { placeID := c.Param("id") // Get the place ID from the URL parameter var payload struct { // Define an anonymous struct for the incoming JSON payload WebsiteURL string `json:"website_url"` // Matches payload from frontend } if err := c.BindJSON(&payload); err != nil { log.Printf("[ERROR]: Failed to bind JSON for website update: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) return } log.Printf("[LOG]: Received website update payload for Place ID %s: %+v", placeID, payload) // Parse the placeID string to uuid.UUID placeUUID, err := uuid.Parse(placeID) if err != nil { log.Printf("[ERROR]: Invalid Place ID UUID format for website update '%s': %s", placeID, err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Place ID format."}) return } // Find the place by ID and update its WebsiteURL // Note: models.Place must have WebsiteURL field mapped to website_url in DB result := database.DB.Model(&models.Place{}).Where("id = ?", placeUUID).Update("WebsiteURL", payload.WebsiteURL) if result.Error != nil { log.Printf("[ERROR]: Error updating website for place %s in DB: %s", placeID, result.Error) c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) return } if result.RowsAffected == 0 { log.Printf("[WARN]: No place found with ID %s for website update.", placeID) c.JSON(http.StatusNotFound, gin.H{"error": "Place not found."}) return } c.JSON(http.StatusOK, gin.H{"message": "Website updated successfully!"}) } // Helper function to validate image files func isValidImage(file *multipart.FileHeader) bool { allowedTypes := []string{"image/jpeg", "image/png", "image/gif"} contentType := file.Header.Get("Content-Type") for _, t := range allowedTypes { if t == contentType { return true } } return false } // ListEndpoints function (from your provided routes.go, for debugging/listing files) func ListEndpoints(c *gin.Context) { dirPath := "./uploads" entries, err := os.ReadDir(dirPath) if err != nil { log.Println("Error reading directory:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to read uploads directory: %s", err)}) return } var fileNames []string for _, entry := range entries { if !entry.IsDir() { fileNames = append(fileNames, entry.Name()) } } c.JSON(http.StatusOK, gin.H{"files": fileNames, "message": "List of files in uploads directory."}) } func toFrontendReview(review models.Review) FrontendReview { var user models.User var place models.Place if err := database.DB.First(&user, review.Reviewer).Error; err != nil { log.Printf("[WARN]: User with ID %s not found for review %s: %s", review.Reviewer, review.ID, err) user.Username = "Unknown User" } if err := database.DB.First(&place, review.Place).Error; err != nil { log.Printf("[WARN]: Place with ID %s not found for review %s: %s", review.Place, review.ID, err) place.Name = "Unknown Place" } newReview := FrontendReview{ User: user.Username, Place: place.Name, Quiet: review.Quiet, Clean: review.Clean, CleanWudu: review.CleanWudu, Private: review.Private, ChildFriendly: review.ChildFriendly, Safe: review.Safe, Notes: review.Notes, } return newReview } func toFrontendPlace(place models.Place) FrontendPlace { var reviews []models.Review var images []models.PlaceImage database.DB.Where("place = ?", place.ID).Find(&reviews) database.DB.Where("place_id = ?", place.ID).Find(&images) newPlace := FrontendPlace{ ID: place.ID, Name: place.Name, Latitude: place.Latitude, Longitude: place.Longitude, Address: place.Address, LocationType: place.LocationType, WomensSpace: place.WomensSpace, Wudu: place.Wudu, WebsiteURL: place.WebsiteURL, Notes: place.Notes, Images: images, Reviews: reviews, } return newPlace }