448 lines
14 KiB
Go

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
}