263 lines
7.4 KiB
Go
Executable File
263 lines
7.4 KiB
Go
Executable File
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
db "git.nochill.in/nochill/hiling_go/db/repository"
|
|
"git.nochill.in/nochill/hiling_go/util"
|
|
"git.nochill.in/nochill/hiling_go/util/token"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type createReviewReq struct {
|
|
SubmittedBy int8 `json:"submitted_by" binding:"required"`
|
|
Comments string `json:"comments" binding:"required"`
|
|
Score int8 `json:"score" binding:"numeric,max=100"`
|
|
IsFromCritic *bool `json:"is_from_critic"`
|
|
IsHided *bool `json:"is_hided"`
|
|
LocationId int32 `json:"location_id" binding:"required"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
// @Summary Create a review
|
|
// @Tags reviews
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security CookieAuth
|
|
// @Param body body createReviewReq true "Review payload"
|
|
// @Success 200 {object} map[string]any
|
|
// @Failure 400 {object} map[string]any
|
|
// @Failure 409 {object} map[string]any
|
|
// @Router /review/location [post]
|
|
func (server *Server) createReview(ctx *gin.Context) {
|
|
var req createReviewReq
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
|
|
checkArg := db.CheckIfReviewExistsParams{
|
|
LocationID: req.LocationId,
|
|
SubmittedBy: int32(req.SubmittedBy),
|
|
}
|
|
|
|
reviewExist, err := server.Store.CheckIfReviewExists(ctx, checkArg)
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, "Something went wrong while try to check review")
|
|
return
|
|
}
|
|
|
|
if reviewExist != 0 {
|
|
ctx.JSON(http.StatusConflict, "User review already exist")
|
|
return
|
|
}
|
|
|
|
arg := db.CreateReviewParams{
|
|
SubmittedBy: int32(req.SubmittedBy),
|
|
Comments: req.Comments,
|
|
Score: int16(req.Score),
|
|
IsFromCritic: *req.IsFromCritic,
|
|
IsHided: *req.IsHided,
|
|
LocationID: req.LocationId,
|
|
Title: pgtype.Text{String: req.Title, Valid: req.Title != ""},
|
|
}
|
|
|
|
review, err := server.Store.CreateReview(ctx, arg)
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save review"))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, review)
|
|
}
|
|
|
|
// @Summary Upload images for a review
|
|
// @Tags reviews
|
|
// @Accept mpfd
|
|
// @Produce json
|
|
// @Security CookieAuth
|
|
// @Param review_id formData int true "Review ID"
|
|
// @Param images formData file true "Image files"
|
|
// @Success 200 {object} map[string]any
|
|
// @Failure 400 {object} map[string]any
|
|
// @Router /review/location/images [post]
|
|
func (server *Server) uploadReviewImages(ctx *gin.Context) {
|
|
reviewIdStr := ctx.PostForm("review_id")
|
|
reviewId, err := strconv.Atoi(reviewIdStr)
|
|
if err != nil || reviewId <= 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"message": "Invalid review_id"})
|
|
return
|
|
}
|
|
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ErrorResponse(err, "Failed to parse multipart form"))
|
|
return
|
|
}
|
|
|
|
files := form.File["images"]
|
|
if len(files) == 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"message": "No images provided"})
|
|
return
|
|
}
|
|
|
|
var params []db.CreateImageParams
|
|
|
|
for _, fileHeader := range files {
|
|
fileExt := filepath.Ext(fileHeader.Filename)
|
|
key := fmt.Sprintf("reviews/%d/%s%d%s", reviewId, util.RandomString(5), time.Now().UnixNano(), fileExt)
|
|
|
|
f, err := fileHeader.Open()
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to open image"))
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
contentType := fileHeader.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
|
|
publicURL, err := server.R2.UploadFile(ctx.Request.Context(), key, f, contentType)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to upload image to storage"))
|
|
return
|
|
}
|
|
|
|
params = append(params, db.CreateImageParams{
|
|
ImageUrl: publicURL,
|
|
UploadedBy: int32(authPayload.UserID),
|
|
ImageType: "reviews",
|
|
ImageOf: int32(reviewId),
|
|
})
|
|
}
|
|
|
|
if err := server.Store.CreateImage(ctx, params); err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to save image records"))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"uploaded": len(params)})
|
|
}
|
|
|
|
type getReviewReq struct {
|
|
LocationID int32 `uri:"location_id" binding:"required"`
|
|
ReviewID int32 `uri:"review_id" binding:"required"`
|
|
}
|
|
|
|
type getReviewRes struct {
|
|
db.GetReviewByLocationAndId
|
|
Images []db.GetImagesByReviewRow `json:"images"`
|
|
}
|
|
|
|
// @Summary Get a single review
|
|
// @Tags reviews
|
|
// @Produce json
|
|
// @Param location_id path int true "Location ID"
|
|
// @Param review_id path int true "Review ID"
|
|
// @Success 200 {object} map[string]any
|
|
// @Failure 404 {object} map[string]any
|
|
// @Router /location/{location_id}/review/{review_id} [get]
|
|
func (server *Server) getReview(ctx *gin.Context) {
|
|
var req getReviewReq
|
|
|
|
if err := ctx.ShouldBindUri(&req); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
|
|
arg := db.GetReviewByLocationAndIdParams{
|
|
ID: req.ReviewID,
|
|
LocationID: req.LocationID,
|
|
}
|
|
|
|
review, err := server.Store.GetReviewByLocationAndId(ctx, arg)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
ctx.JSON(http.StatusNotFound, ErrorResponse(err, "Review not found"))
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while trying to retrieve review"))
|
|
return
|
|
}
|
|
|
|
images, err := server.Store.GetImagesByReview(ctx, review.ID)
|
|
if err != nil {
|
|
images = []db.GetImagesByReviewRow{}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, getReviewRes{
|
|
GetReviewByLocationAndId: review,
|
|
Images: images,
|
|
})
|
|
}
|
|
|
|
type getUserReviewByLocationReq struct {
|
|
LocationID int32 `uri:"location_id" binding:"required"`
|
|
}
|
|
|
|
type userReviewWithImagesResponse struct {
|
|
db.GetUserReviewByLocationRow
|
|
Images []db.GetImagesByReviewRow `json:"images"`
|
|
}
|
|
|
|
// @Summary Get current user's review for a location
|
|
// @Tags reviews
|
|
// @Produce json
|
|
// @Security CookieAuth
|
|
// @Param location_id path int true "Location ID"
|
|
// @Success 200 {object} map[string]any
|
|
// @Failure 404 {object} map[string]any
|
|
// @Router /user/review/location/{location_id} [get]
|
|
func (server *Server) getUserReviewByLocation(ctx *gin.Context) {
|
|
var req getUserReviewByLocationReq
|
|
|
|
if err := ctx.ShouldBindUri(&req); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
log.Println(authPayload)
|
|
|
|
arg := db.GetUserReviewByLocationParams{
|
|
SubmittedBy: int32(authPayload.UserID),
|
|
LocationID: req.LocationID,
|
|
}
|
|
|
|
review, err := server.Store.GetUserReviewByLocation(ctx, arg)
|
|
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
ctx.JSON(http.StatusNotFound, ErrorResponse(err, "Review not found"))
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to recevie current user review"))
|
|
return
|
|
}
|
|
|
|
images, err := server.Store.GetImagesByReview(ctx, review.ID)
|
|
if err != nil {
|
|
images = []db.GetImagesByReviewRow{}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, userReviewWithImagesResponse{
|
|
GetUserReviewByLocationRow: review,
|
|
Images: images,
|
|
})
|
|
}
|