448 lines
14 KiB
Go
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
|
|
}
|