diff --git a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx index 204b5f4b..2ea6f3a5 100644 --- a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx +++ b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx @@ -26,6 +26,11 @@ The API also allows you to retrieve information about these resources. - [Get Security Settings](#GetSecuritySettings) - [Update Notification Settings](#UpdateNotificationSettings) - [Get Notification Settings](#GetNotificationSettings) + - [Upload Profile Image](#UploadProfileImage) + - [Download Profile Image](#DownloadProfileImage) + - [Image ID](#ImageID) + - [Upload Image](#UploadImage) + - [Upload Room Image](#UploadRoomImage) ## Api @@ -718,3 +723,184 @@ or - **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Failed to update settings"} }` + +### UploadProfileImage + +This endpoint is used to upload a profile image for a user in the Occupi system. Images must be in jpg, jpeg, or png format otherwise an error will be returned. + +- **URL** + + `/api/upload-profile-image` + +- **Method** + + `POST` + +- **Request Form Data** + +- **Content** + +```copy + "image": image_data, // required + "email": "abcd@gmail.com" // optional if you are logged in +``` + +**Success Response** + +- **Code:** 200 + +- **Content:** `{ "status": 200, "message": "Successfully uploaded image!", "data": null }` + +**Error Response** + +- **Code:** 500 + +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` + +### DownloadProfileImage + +This endpoint is used to download a profile image for a user in the Occupi system. + +- **URL** + + `/api/download-profile-image` + +- **Method** + + `GET` + +- **Request JSON Data** + +- **Content** + +```json copy + "email": "abcd@gmail.com" // optional if you are logged in + "quality": "mid" // optional, can be "thumbnail", "low", "medium", "high but defaults to mid +``` + +or + +``` +/api/download-profile-image?email=abcd@gmail.com&quality=mid +``` + +**note that the higher quality, the longer it will take to download** + +**Success Response** + +- **Code:** 200 + +- **Content:** `{ "status": 200, "message": "Successfully fetched image!", "data": null }` + +**Error Response** + +- **Code:** 500 + +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` + +### Image ID + +This endpoint is used to get the image in the Occupi system given an image ID as a url parameter. + +- **URL** + + `/api/image/:id` + +- **Method** + + `GET` + +- **Request JSON Data** + +- **Content** + +```json copy + "quality": "mid" // optional, can be "thumbnail", "low", "medium", "high but defaults to mid +``` + +or + +``` +/api/image/000000000000000000?quality=mid +``` + +**the id is the id of the image which you will get when you get a room and is required for all requests on this endpoint** + +**note that the higher quality, the longer it will take to download** + +**Success Response** + +- **Code:** 200 + +- **Content:** `{ "status": 200, "message": "Successfully fetched image!", "data": null }` + +**Error Response** + +- **Code:** 500 + +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` + +### Upload Image + +This endpoint is used to upload an image in the Occupi system. Only Admins can upload images. + +- **URL** + + `/api/upload-image` + +- **Method** + + `POST` + +- **Request Form Data** + +- **Content** + +```copy + "image": image_data, // required +``` + +**Success Response** + +- **Code:** 200 + +- **Content:** `{ "status": 200, "message": "Successfully uploaded image!", "data": {"id": "000000000000000000000"} }` + +**Error Response** + +- **Code:** 500 + +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` + +### Upload Room Image + +This endpoint is used to upload an image for a room in the Occupi system. Only Admins can upload images. + +- **URL** + + `/api/upload-room-image` + +- **Method** + + `POST` + +- **Request Form Data** + +- **Content** + +```copy + "image": image_data, // required + "roomId": "RM000" // required +``` + +**Success Response** + +- **Code:** 200 + +- **Content:** `{ "status": 200, "message": "Successfully uploaded image!", "data": {"id": "000000000000000000000"} }` + +**Error Response** + +- **Code:** 500 + +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` \ No newline at end of file diff --git a/occupi-backend/go.mod b/occupi-backend/go.mod index b64c1356..81ebe3e7 100644 --- a/occupi-backend/go.mod +++ b/occupi-backend/go.mod @@ -25,6 +25,7 @@ require ( require ( github.com/golang/protobuf v1.5.3 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/rabbitmq/amqp091-go v1.10.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect diff --git a/occupi-backend/go.sum b/occupi-backend/go.sum index 6a4e95c9..751a36b3 100644 --- a/occupi-backend/go.sum +++ b/occupi-backend/go.sum @@ -100,6 +100,8 @@ github.com/newrelic/go-agent/v3 v3.33.1 h1:eWOtty43cyxrMKws4VNPdebgEB6ujFTf0yxPs github.com/newrelic/go-agent/v3 v3.33.1/go.mod h1:SMdqPzE/ghkWdY0rYGSD7Clw2daK/XH6pUnVd4albg4= github.com/newrelic/go-agent/v3/integrations/nrgin v1.3.1 h1:JoIIS37S/tW/6Y/5cGInNA7ngfd2lGsfX0HYh82UOPk= github.com/newrelic/go-agent/v3/integrations/nrgin v1.3.1/go.mod h1:/EjSYMXsMO34+imRoQlPU5hFUlP+u/dwowF+OKMw8TQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oliveroneill/exponent-server-sdk-golang v0.0.0-20210823140141-d050598be512 h1:/ZSmjwl1inqsiHMhn+sPlEtSHdVTf+TH3LNGGdMQ/vA= github.com/oliveroneill/exponent-server-sdk-golang v0.0.0-20210823140141-d050598be512/go.mod h1:Isv/48UnAjtxS8FD80Bito3ZJqZRyIMxKARIEITfW4k= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= diff --git a/occupi-backend/pkg/constants/constants.go b/occupi-backend/pkg/constants/constants.go index 86d83a8f..2d0e10b3 100644 --- a/occupi-backend/pkg/constants/constants.go +++ b/occupi-backend/pkg/constants/constants.go @@ -7,6 +7,7 @@ const ( IncompleteAuthCode = "INCOMPLETE_AUTH" InternalServerErrorCode = "INTERNAL_SERVER_ERROR" UnAuthorizedCode = "UNAUTHORIZED" + RequestEntityTooLargeCode = "REQUEST_ENTITY_TOO_LARGE" Admin = "admin" Basic = "basic" AdminDBAccessOption = "authSource=admin" @@ -22,4 +23,12 @@ const ( ConfirmIPAddress = "confirmIPAddress" Off = "off" On = "on" + ThumbnailRes = "thumbnail" + LowRes = "low" + MidRes = "mid" + HighRes = "high" + ThumbnailWidth = 200 + LowWidth = 600 + MidWidth = 1200 + HighWidth = 2000 ) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index eaba4380..c2cf2367 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -987,11 +987,18 @@ func MarkNotificationAsSent(ctx context.Context, appsession *models.AppSession, collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Notifications") + id, err := primitive.ObjectIDFromHex(notificationID) + + if err != nil { + logrus.Error(err) + return err + } + // update the notification to sent - filter := bson.M{"_id": notificationID} + filter := bson.M{"notificationId": id} update := bson.M{"$set": bson.M{"sent": true}} - _, err := collection.UpdateOne(ctx, filter, update) + _, err = collection.UpdateOne(ctx, filter, update) if err != nil { logrus.Error(err) return err @@ -1262,3 +1269,151 @@ func UpdateNotificationSettings(ctx *gin.Context, appsession *models.AppSession, return nil } + +func UploadImageData(ctx *gin.Context, appsession *models.AppSession, image models.Image) (string, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return "", errors.New("database is nil") + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Images") + + id, err := collection.InsertOne(ctx, image) + if err != nil { + logrus.Error(err) + return "", err + } + + return id.InsertedID.(primitive.ObjectID).Hex(), nil +} + +func GetImageData(ctx *gin.Context, appsession *models.AppSession, imageID string, quality string) (models.Image, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return models.Image{}, errors.New("database is nil") + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Images") + + id, err := primitive.ObjectIDFromHex(imageID) + + if err != nil { + logrus.Error(err) + return models.Image{}, err + } + + filter := bson.M{"_id": id} + + // add quality attribute to projection + findOptions := options.FindOne() + findOptions.SetProjection(bson.M{"image_" + quality + "_res": 1, "_id": 0, "fileName": 1}) + + var image models.Image + err = collection.FindOne(ctx, filter, findOptions).Decode(&image) + if err != nil { + logrus.WithError(err).Error("Failed to get image data") + return models.Image{}, err + } + + return image, nil +} + +func DeleteImageData(ctx *gin.Context, appsession *models.AppSession, imageID string) error { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return errors.New("database is nil") + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Images") + + filter := bson.M{"_id": imageID} + _, err := collection.DeleteOne(ctx, filter) + if err != nil { + logrus.Error(err) + return err + } + + return nil +} + +func SetUserImage(ctx *gin.Context, appsession *models.AppSession, email, imageID string) error { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return errors.New("database is nil") + } + + // get user from cache + userData, cacheErr := cache.GetUser(appsession, email) + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") + + filter := bson.M{"email": email} + update := bson.M{"$set": bson.M{"details.imageid": imageID}} + + _, err := collection.UpdateOne(ctx, filter, update) + if err != nil { + logrus.Error(err) + return err + } + + // update user in cache + if cacheErr == nil { + userData.Details.ImageID = imageID + cache.SetUser(appsession, userData) + } + + return nil +} + +func GetUserImage(ctx *gin.Context, appsession *models.AppSession, email string) (string, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return "", errors.New("database is nil") + } + + // check if user is in cache + if userData, err := cache.GetUser(appsession, email); err == nil { + return userData.Details.ImageID, nil + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") + + filter := bson.M{"email": email} + var user models.User + err := collection.FindOne(ctx, filter).Decode(&user) + if err != nil { + logrus.WithError(err).Error("Failed to get user image id") + return "", err + } + + // Add the user to the cache if cache is not nil + cache.SetUser(appsession, user) + + return user.Details.ImageID, nil +} + +func AddImageIDToRoom(ctx *gin.Context, appsession *models.AppSession, roomID, imageID string) error { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return errors.New("database is nil") + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Rooms") + + filter := bson.M{"roomId": roomID} + update := bson.M{"$addToSet": bson.M{"roomImageIds": imageID}} + + _, err := collection.UpdateOne(ctx, filter, update) + if err != nil { + logrus.Error(err) + return err + } + + return nil +} diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index a5400f6a..4984fc7d 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/json" "errors" + "fmt" "net/http" "reflect" "strconv" @@ -683,3 +684,259 @@ func GetNotificationSettings(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched notification settings!", notificationSettings)) } + +func UploadProfileImage(ctx *gin.Context, appsession *models.AppSession) { + file, err := ctx.FormFile("image") + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Invalid file format", + nil)) + return + } + + var requestEmail models.RequestEmail + if err := ctx.ShouldBindJSON(&requestEmail); err != nil { + email, err := AttemptToGetEmail(ctx, appsession) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Email must be provided", + nil)) + return + } + requestEmail.Email = email + } + + // get user image if it exists and delete it + id, err := database.GetUserImage(ctx, appsession, requestEmail.Email) + if err == nil { + err = database.DeleteImageData(ctx, appsession, id) + if err != nil { + logrus.WithError(err).Error("Failed to delete user image") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + } + + // convert to bytes + fileBytesThumbnail, errThumbnail := utils.ConvertImageToBytes(file, constants.ThumbnailWidth, true) + fileBytesLow, errLow := utils.ConvertImageToBytes(file, constants.LowWidth, false) + fileBytesMid, errMid := utils.ConvertImageToBytes(file, constants.MidWidth, false) + fileBytesHigh, errHigh := utils.ConvertImageToBytes(file, constants.HighWidth, false) + + if err != nil || errThumbnail != nil || errLow != nil || errMid != nil || errHigh != nil { + logrus.WithError(err).Error("Failed to convert image to bytes") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + // Create a ProfileImage document + profileImage := models.Image{ + FileName: file.Filename, + Thumbnail: fileBytesThumbnail, + ImageLowRes: fileBytesLow, + ImageMidRes: fileBytesMid, + ImageHighRes: fileBytesHigh, + } + + // Save the image to the database + newID, err := database.UploadImageData(ctx, appsession, profileImage) + + if err != nil { + logrus.WithError(err).Error("Failed to upload image data") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + // Update the user details with the image id + err = database.SetUserImage(ctx, appsession, requestEmail.Email, newID) + + if err != nil { + logrus.WithError(err).Error("Failed to set user image") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully uploaded image!", nil)) +} + +func DownloadProfileImage(ctx *gin.Context, appsession *models.AppSession) { + var request models.ProfileImageRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + email := ctx.Query("email") + quality := ctx.Query("quality") + if email == "" { + email, err := AttemptToGetEmail(ctx, appsession) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Email must be provided", + nil)) + return + } + request.Email = email + } else { + request.Email = email + } + request.Quality = quality + + } + + if request.Quality != "" && request.Quality != constants.ThumbnailRes && request.Quality != constants.LowRes && request.Quality != constants.MidRes && request.Quality != constants.HighRes { + request.Quality = constants.MidRes + } else if request.Quality == "" { + request.Quality = constants.MidRes + } + + // get the image id + id, err := database.GetUserImage(ctx, appsession, request.Email) + if err != nil { + logrus.WithError(err).Error("Failed to get user image") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + // get the image data + imageData, err := database.GetImageData(ctx, appsession, id, request.Quality) + if err != nil { + logrus.WithError(err).Error("Failed to get image data") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + // set the response headers + ctx.Header("Content-Disposition", "attachment; filename="+imageData.FileName) + ctx.Header("/Content-Type", "application/octet-stream") + switch request.Quality { + case constants.ThumbnailRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.Thumbnail) + case constants.LowRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageLowRes) + case constants.MidRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageMidRes) + case constants.HighRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageHighRes) + default: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageMidRes) + } +} + +func DownloadImage(ctx *gin.Context, appsession *models.AppSession) { + var request models.ImageRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + request.ID = ctx.Param("id") + request.Quality = ctx.Query("quality") + if request.ID == "" { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "ID must be provided", + nil)) + return + } + } + + if request.Quality != "" && request.Quality != constants.ThumbnailRes && request.Quality != constants.LowRes && request.Quality != constants.MidRes && request.Quality != constants.HighRes { + request.Quality = constants.MidRes + } else if request.Quality == "" { + request.Quality = constants.MidRes + } + + // print the request + fmt.Println(request) + + // get the image data + imageData, err := database.GetImageData(ctx, appsession, request.ID, request.Quality) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get image", constants.InternalServerErrorCode, "Failed to get image", nil)) + return + } + + // set the response headers + ctx.Header("Content-Disposition", "attachment; filename="+imageData.FileName) + ctx.Header("/Content-Type", "application/octet-stream") + switch request.Quality { + case constants.ThumbnailRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.Thumbnail) + case constants.LowRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageLowRes) + case constants.MidRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageMidRes) + case constants.HighRes: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageHighRes) + default: + ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageMidRes) + } +} + +func UploadImage(ctx *gin.Context, appsession *models.AppSession, roomUpload bool) { + file, err := ctx.FormFile("image") + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Invalid file format", + nil)) + return + } + + // convert to bytes + fileBytesThumbnail, errThumbnail := utils.ConvertImageToBytes(file, constants.ThumbnailWidth, true) + fileBytesLow, errLow := utils.ConvertImageToBytes(file, constants.LowWidth, false) + fileBytesMid, errMid := utils.ConvertImageToBytes(file, constants.MidWidth, false) + fileBytesHigh, errHigh := utils.ConvertImageToBytes(file, constants.HighWidth, false) + + if errThumbnail != nil || errLow != nil || errMid != nil || errHigh != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to convert file to bytes", constants.InternalServerErrorCode, "Failed to convert file to bytes", nil)) + return + } + + // Create a ProfileImage document + profileImage := models.Image{ + FileName: file.Filename, + Thumbnail: fileBytesThumbnail, + ImageLowRes: fileBytesLow, + ImageMidRes: fileBytesMid, + ImageHighRes: fileBytesHigh, + } + + // Save the image to the database + newID, err := database.UploadImageData(ctx, appsession, profileImage) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to upload image", constants.InternalServerErrorCode, "Failed to upload image", nil)) + return + } + + if roomUpload { + // get room id from json body + roomid := ctx.Query("roomid") + + if roomid == "" { + roomid = ctx.PostForm("roomid") + if roomid == "" { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid JSON payload", nil)) + return + } + } + + // Update the room details with the image id + err = database.AddImageIDToRoom(ctx, appsession, roomid, newID) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to update room image", constants.InternalServerErrorCode, "Failed to update room image", nil)) + return + } + } + + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully uploaded image!", gin.H{"id": newID})) +} diff --git a/occupi-backend/pkg/middleware/middleware.go b/occupi-backend/pkg/middleware/middleware.go index 3c6a18e7..bc98de92 100644 --- a/occupi-backend/pkg/middleware/middleware.go +++ b/occupi-backend/pkg/middleware/middleware.go @@ -1,6 +1,7 @@ package middleware import ( + "fmt" "net" "net/http" "strings" @@ -200,3 +201,23 @@ func RealIPMiddleware() gin.HandlerFunc { ctx.Next() } } + +// LimitRequestBodySize middleware to limit the size of the request body +func LimitRequestBodySize(maxSize int64) gin.HandlerFunc { + return func(ctx *gin.Context) { + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxSize) + if err := ctx.Request.ParseMultipartForm(maxSize); err != nil { + ctx.JSON(http.StatusRequestEntityTooLarge, utils.ErrorResponse( + http.StatusRequestEntityTooLarge, + "Request Entity Too Large", + constants.RequestEntityTooLargeCode, + fmt.Sprintf("Request body too large by %d bytes, max %d bytes", ctx.Request.ContentLength-maxSize, maxSize), + nil, + ), + ) + ctx.Abort() + return + } + ctx.Next() + } +} diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 630119dd..c75062e5 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -52,6 +52,7 @@ type FilterUsers struct { } type Details struct { + ImageID string `json:"imageid" bson:"imageid"` // image id in image collection ContactNo string `json:"contactNo" bson:"contactNo"` Name string `json:"name" bson:"name"` DOB time.Time `json:"dob" bson:"dob"` @@ -121,14 +122,15 @@ type ViewBookings struct { } type Room struct { - ID string `json:"_id" bson:"_id,omitempty"` - RoomID string `json:"roomId" bson:"roomId,omitempty"` - RoomNo string `json:"roomNo" bson:"roomNo,omitempty"` - FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` - MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` - MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy"` - Description string `json:"description" bson:"description"` - RoomName string `json:"roomName" bson:"roomName"` + ID string `json:"_id" bson:"_id,omitempty"` + RoomID string `json:"roomId" bson:"roomId,omitempty"` + RoomNo string `json:"roomNo" bson:"roomNo,omitempty"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` + MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy"` + Description string `json:"description" bson:"description"` + RoomName string `json:"roomName" bson:"roomName"` + RoomImageIDs []string `json:"roomImageIds" bson:"roomImageIds"` } type ResetToken struct { @@ -155,3 +157,12 @@ type FilterStruct struct { Skip int64 Sort primitive.M } + +type Image struct { + ID string `json:"_id" bson:"_id,omitempty"` + Thumbnail []byte `json:"image_thumbnail_res" bson:"image_thumbnail_res"` + ImageLowRes []byte `json:"image_low_res" bson:"image_low_res"` + ImageMidRes []byte `json:"image_mid_res" bson:"image_mid_res"` + ImageHighRes []byte `json:"image_high_res" bson:"image_high_res"` + FileName string `json:"fileName" bson:"fileName"` +} diff --git a/occupi-backend/pkg/models/request.go b/occupi-backend/pkg/models/request.go index e63de5f4..a807c15b 100644 --- a/occupi-backend/pkg/models/request.go +++ b/occupi-backend/pkg/models/request.go @@ -82,6 +82,12 @@ type NotificationsRequest struct { BookingReminder string `json:"bookingReminder"` } -type ImageUploadRequest struct { - Image string `json:"image" binding:"required"` +type ProfileImageRequest struct { + Email string `json:"email" binding:"omitempty,email"` + Quality string `json:"quality"` +} + +type ImageRequest struct { + ID string `json:"id" binding:"required"` + Quality string `json:"quality"` } diff --git a/occupi-backend/pkg/receiver/recieve.go b/occupi-backend/pkg/receiver/recieve.go index 515730ad..6b7ea14f 100644 --- a/occupi-backend/pkg/receiver/recieve.go +++ b/occupi-backend/pkg/receiver/recieve.go @@ -44,16 +44,17 @@ func StartConsumeMessage(appsession *models.AppSession) { go func() { for d := range msgs { parts := strings.Split(string(d.Body), "|") - if len(parts) != 6 { + if len(parts) != 7 { continue } - title, message, sendTimeStr, unsentExpoTokens, emails, unreadEmails := parts[0], parts[1], parts[2], parts[3], parts[4], parts[5] + ID, title, message, sendTimeStr, unsentExpoTokens, emails, unreadEmails := parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] sendTime, err := time.Parse(time.RFC3339, sendTimeStr) if err != nil { continue } notification := models.ScheduledNotification{ + ID: ID, Title: title, Message: message, SendTime: sendTime, diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index 31eaa591..333ec651 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -48,6 +48,12 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { api.GET("/update-notification-settings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.UpdateNotificationSettings(ctx, appsession) }) api.GET("/get-security-settings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetSecuritySettings(ctx, appsession) }) api.GET("/get-notification-settings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetNotificationSettings(ctx, appsession) }) + // limit request body size to 16MB when uploading profile image due to mongoDB document size limit + api.POST("/upload-profile-image", middleware.ProtectedRoute, middleware.LimitRequestBodySize(16<<20), func(ctx *gin.Context) { handlers.UploadProfileImage(ctx, appsession) }) + api.GET("/download-profile-image", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.DownloadProfileImage(ctx, appsession) }) + api.GET("/image/:id", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.DownloadImage(ctx, appsession) }) + api.POST("/upload-image", middleware.ProtectedRoute, middleware.AdminRoute, middleware.LimitRequestBodySize(16<<20), func(ctx *gin.Context) { handlers.UploadImage(ctx, appsession, false) }) + api.POST("/upload-room-image", middleware.ProtectedRoute, middleware.AdminRoute, middleware.LimitRequestBodySize(16<<20), func(ctx *gin.Context) { handlers.UploadImage(ctx, appsession, true) }) } auth := router.Group("/auth") { diff --git a/occupi-backend/pkg/sender/send.go b/occupi-backend/pkg/sender/send.go index c0fb60cc..b808453f 100644 --- a/occupi-backend/pkg/sender/send.go +++ b/occupi-backend/pkg/sender/send.go @@ -22,7 +22,8 @@ func PublishMessage(appsession *models.AppSession, notification models.Scheduled defer cancel() body := fmt.Sprintf( - "%s|%s|%s|%s|%s|%s", + "%s|%s|%s|%s|%s|%s|%s", + notification.ID, notification.Title, notification.Message, notification.SendTime.Format(time.RFC3339), diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 2397e526..6fe5bc0e 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -1,11 +1,17 @@ package utils import ( + "bytes" "crypto/rand" "errors" "fmt" + "image" + "image/jpeg" + "image/png" "log" + "mime/multipart" "os" + "path/filepath" "reflect" "regexp" "strings" @@ -16,6 +22,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator" "github.com/microcosm-cc/bluemonday" + "github.com/nfnt/resize" "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -571,3 +578,59 @@ func GetClientTime(ctx *gin.Context) time.Time { return time.Now().In(loc.(*time.Location)) } + +func ConvertImageToBytes(file *multipart.FileHeader, width uint, thumbnail bool) ([]byte, error) { + const ( + pngExt = ".png" + jpgExt = ".jpg" + jpegExt = ".jpeg" + ) + // Check the file extension + ext := filepath.Ext(file.Filename) + if ext != jpegExt && ext != jpgExt && ext != pngExt { + return nil, errors.New("unsupported file type") + } + + src, err := file.Open() + if err != nil { + return nil, err + } + defer src.Close() + + // Decode the image + var img image.Image + switch ext { + case jpegExt, jpgExt: + img, err = jpeg.Decode(src) + if err != nil { + return nil, err + } + case pngExt: + img, err = png.Decode(src) + if err != nil { + return nil, err + } + } + + // Resize the image + var m image.Image + if !thumbnail { + m = resize.Resize(width, 0, img, resize.NearestNeighbor) + } else { + m = resize.Thumbnail(200, 200, img, resize.NearestNeighbor) + } + + // Convert the image to bytes + buf := new(bytes.Buffer) + switch ext { + case jpegExt, jpgExt: + err = jpeg.Encode(buf, m, nil) + case pngExt: + err = png.Encode(buf, m) + } + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +}