From 2b427a244eb7ca45940cbfab3effc5631d30399f Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Fri, 19 Jul 2024 21:01:31 +0200 Subject: [PATCH 1/2] chore: rewrote some backend logix and added some new endpoints --- occupi-backend/pkg/constants/constants.go | 2 + occupi-backend/pkg/database/database.go | 445 ++++++++++++-------- occupi-backend/pkg/handlers/api_handlers.go | 261 ++++++++---- occupi-backend/pkg/handlers/auth_helpers.go | 14 + occupi-backend/pkg/models/database.go | 5 +- occupi-backend/pkg/models/request.go | 23 +- occupi-backend/pkg/router/router.go | 11 +- occupi-backend/tests/handlers_test.go | 5 +- 8 files changed, 507 insertions(+), 259 deletions(-) diff --git a/occupi-backend/pkg/constants/constants.go b/occupi-backend/pkg/constants/constants.go index a7ea6690..86d83a8f 100644 --- a/occupi-backend/pkg/constants/constants.go +++ b/occupi-backend/pkg/constants/constants.go @@ -20,4 +20,6 @@ const ( ChangePassword = "changePassword" ChangeEmail = "changeEmail" ConfirmIPAddress = "confirmIPAddress" + Off = "off" + On = "on" ) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 91e946f5..2d06cbb5 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -68,34 +68,6 @@ func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models return true, nil } -// Retrieves bookings associated with a user -func GetUserBookings(ctx *gin.Context, appsession *models.AppSession, email string) ([]models.Booking, error) { - // Get the bookings for the user - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") - filter := bson.M{ - "$or": []bson.M{ - {"emails": bson.M{"$elemMatch": bson.M{"$eq": email}}}, - {"creator": email}, - }, - } - cursor, err := collection.Find(ctx, filter) - if err != nil { - logrus.Error(err) - return nil, err - } - defer cursor.Close(ctx) - var bookings []models.Booking - for cursor.Next(ctx) { - var booking models.Booking - if err := cursor.Decode(&booking); err != nil { - logrus.Error(err) - return nil, err - } - bookings = append(bookings, booking) - } - return bookings, nil -} - // Confirms the user check-in by checking certain criteria func ConfirmCheckIn(ctx *gin.Context, appsession *models.AppSession, checkIn models.CheckIn) (bool, error) { // Save the check-in to the database @@ -516,170 +488,124 @@ func ConfirmCancellation(ctx *gin.Context, appsession *models.AppSession, id str return true, nil } -// Gets all rooms available for booking -func GetAllRooms(ctx *gin.Context, appsession *models.AppSession, floorNo string) ([]models.Room, error) { - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Rooms") - - var cursor *mongo.Cursor - var err error - - // findOptions := options.Find() - // findOptions.SetLimit(10) // Limit the results to 10 - // findOptions.SetSkip(int64(10)) // Skip the specified number of documents for pagination +// Get user information +func GetUserDetails(ctx *gin.Context, appsession *models.AppSession, email string) (models.UserDetailsRequest, error) { + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") - // Find all rooms on the specified floor - filter := bson.M{"floorNo": floorNo} - // cursor, err = collection.Find(context.TODO(), filter, findOptions) - cursor, err = collection.Find(context.TODO(), filter) + // check if user is in cache + if userData, err := cache.GetUser(appsession, email); err == nil { + return models.UserDetailsRequest{ + Email: userData.Email, + Name: userData.Details.Name, + Dob: userData.Details.DOB.String(), + Gender: userData.Details.Gender, + SessionEmail: userData.Email, + Employeeid: userData.OccupiID, + Number: userData.Details.ContactNo, + Pronouns: userData.Details.Pronouns, + }, nil + } + filter := bson.M{"email": email} + var user models.User + err := collection.FindOne(ctx, filter).Decode(&user) if err != nil { logrus.Error(err) - return nil, err - } - defer cursor.Close(context.TODO()) - - var rooms []models.Room - for cursor.Next(context.TODO()) { - var room models.Room - if err := cursor.Decode(&room); err != nil { - logrus.Error(err) - return nil, err - } - rooms = append(rooms, room) + return models.UserDetailsRequest{}, err } - if err := cursor.Err(); err != nil { - logrus.Error(err) - return nil, err - } + // Add the user to the cache if cache is not nil + cache.SetUser(appsession, user) - return rooms, nil + return models.UserDetailsRequest{ + Email: user.Email, + Name: user.Details.Name, + Dob: user.Details.DOB.String(), + Gender: user.Details.Gender, + SessionEmail: user.Email, + Employeeid: user.OccupiID, + Number: user.Details.ContactNo, + Pronouns: user.Details.Pronouns, + }, nil } -// Get user information -func GetUserDetails(ctx *gin.Context, appsession *models.AppSession, email string) (models.UserDetails, error) { +// UpdateUserDetails updates the user's details +func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession, user models.UserDetailsRequest) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") + filter := bson.M{"email": user.SessionEmail} + update := bson.M{"$set": bson.M{}} - filter := bson.M{"email": email} - var user models.UserDetails - err := collection.FindOne(ctx, filter).Decode(&user) - if err != nil { - logrus.Error(err) - return models.UserDetails{}, err - } - return user, nil -} + // get user from cache + userData, cachErr := cache.GetUser(appsession, user.SessionEmail) -// AddFieldToUpdateMap adds a field to the update map if it's non-zero or non-nil -func AddFieldToUpdateMap(updateFields bson.M, fieldName string, fieldValue interface{}) { - switch value := fieldValue.(type) { - case string: - if value != "" { - updateFields[fieldName] = value + if user.Name != "" { + update["$set"].(bson.M)["details.name"] = user.Name + if cachErr == nil { + userData.Details.Name = user.Name } - case bool: - updateFields[fieldName] = value - case *bool: - if value != nil { - updateFields[fieldName] = *value + } + if user.Dob != "" { + layout := "2006-01-02" // Go's reference time format + parsedDOB, err := time.Parse(layout, user.Dob) + if err == nil { + update["$set"].(bson.M)["details.dob"] = parsedDOB + if cachErr == nil { + userData.Details.DOB = parsedDOB + } } - case time.Time: - if !value.IsZero() { - updateFields[fieldName] = value + } + if user.Gender != "" { + update["$set"].(bson.M)["details.gender"] = user.Gender + if cachErr == nil { + userData.Details.Gender = user.Gender } - case *models.Details: - if value != nil { - nestedFields := bson.M{} - AddFieldToUpdateMap(nestedFields, "contactNo", value.ContactNo) - AddFieldToUpdateMap(nestedFields, "name", value.Name) - AddFieldToUpdateMap(nestedFields, "dob", value.DOB) - AddFieldToUpdateMap(nestedFields, "gender", value.Gender) - AddFieldToUpdateMap(nestedFields, "pronouns", value.Pronouns) - if len(nestedFields) > 0 { - updateFields[fieldName] = nestedFields - } + } + if user.Email != "" { + update["$set"].(bson.M)["email"] = user.Email + // set their verification status to false + update["$set"].(bson.M)["isVerified"] = false // when they login again, an otp will be sent to verify their email + + if cachErr == nil { + userData.Email = user.Email + userData.IsVerified = false } - case *models.Notifications: - if value != nil { - nestedFields := bson.M{} - AddFieldToUpdateMap(nestedFields, "invites", value.Invites) - AddFieldToUpdateMap(nestedFields, "bookingReminder", value.BookingReminder) - if len(nestedFields) > 0 { - updateFields[fieldName] = nestedFields - } + } + if user.Employeeid != "" { + update["$set"].(bson.M)["occupiId"] = user.Employeeid + if cachErr == nil { + userData.OccupiID = user.Employeeid } - case *models.Security: - if value != nil { - nestedFields := bson.M{} - AddFieldToUpdateMap(nestedFields, "mfa", value.MFA) - AddFieldToUpdateMap(nestedFields, "biometrics", value.Biometrics) - if len(nestedFields) > 0 { - updateFields[fieldName] = nestedFields - } + } + if user.Number != "" { + update["$set"].(bson.M)["details.contactNo"] = user.Number + if cachErr == nil { + userData.Details.ContactNo = user.Number } } -} - -// UpdateUserDetails updates the user's details -func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession, user models.UserDetails) (bool, error) { - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") - filter := bson.M{"email": user.Email} - - var userStruct models.UserDetails - err := collection.FindOne(context.TODO(), filter).Decode(&userStruct) - if err != nil { - logrus.Error("Failed to find user: ", err) - return false, err + if user.Pronouns != "" { + update["$set"].(bson.M)["details.pronouns"] = user.Pronouns + if cachErr == nil { + userData.Details.Pronouns = user.Pronouns + } } - updateFields := bson.M{} - AddFieldToUpdateMap(updateFields, "occupiId", user.OccupiID) - AddFieldToUpdateMap(updateFields, "password", user.Password) - AddFieldToUpdateMap(updateFields, "email", user.Email) - AddFieldToUpdateMap(updateFields, "role", user.Role) - AddFieldToUpdateMap(updateFields, "onSite", user.OnSite) - AddFieldToUpdateMap(updateFields, "isVerified", user.IsVerified) - AddFieldToUpdateMap(updateFields, "nextVerificationDate", user.NextVerificationDate) - AddFieldToUpdateMap(updateFields, "details", user.Details) - AddFieldToUpdateMap(updateFields, "notifications", user.Notifications) - AddFieldToUpdateMap(updateFields, "security", user.Security) - AddFieldToUpdateMap(updateFields, "status", user.Status) - AddFieldToUpdateMap(updateFields, "position", user.Position) - - update := bson.M{"$set": updateFields} - _, err = collection.UpdateOne(ctx, filter, update) + _, err := collection.UpdateOne(ctx, filter, update) if err != nil { logrus.Error("Failed to update user details: ", err) return false, err } - return true, nil -} - -// Returns all users in the database -func GetAllUsers(ctx *gin.Context, appsession *models.AppSession) ([]models.User, error) { - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") - if collection == nil { - logrus.Error("Failed to get collection") - return nil, errors.New("failed to get collection") - } - cursor, err := collection.Find(ctx, bson.M{}) - if err != nil { - logrus.Error(err) - return nil, err - } - defer cursor.Close(ctx) - var users []models.User - for cursor.Next(ctx) { - var user models.User - if err := cursor.Decode(&user); err != nil { - logrus.Error(err) - return nil, err - } - users = append(users, user) + if cachErr == nil { + cache.SetUser(appsession, userData) } - return users, nil + return true, nil } // Checks if a user is an admin @@ -1097,7 +1023,64 @@ func ReadNotifications(ctx *gin.Context, appsession *models.AppSession, email st return nil } +func GetSecuritySettings(ctx *gin.Context, appsession *models.AppSession, email string) (models.SecuritySettingsRequest, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return models.SecuritySettingsRequest{}, errors.New("database is nil") + } + + // check if user is in cache + if userData, err := cache.GetUser(appsession, email); err == nil { + var twofa string + if userData.TwoFAEnabled { + twofa = constants.On + } else { + twofa = constants.Off + } + + return models.SecuritySettingsRequest{ + Email: userData.Email, + Twofa: twofa, + }, 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.Error(err) + return models.SecuritySettingsRequest{}, err + } + + // Add the user to the cache if cache is not nil + cache.SetUser(appsession, user) + + var twofa string + if user.TwoFAEnabled { + twofa = constants.On + } else { + twofa = constants.Off + } + + return models.SecuritySettingsRequest{ + Email: user.Email, + Twofa: twofa, + }, nil +} + func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession, securitySettings models.SecuritySettingsRequest) 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, securitySettings.Email) + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": securitySettings.Email} @@ -1105,12 +1088,137 @@ func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession, sec if securitySettings.NewPassword != "" { update["$set"].(bson.M)["password"] = securitySettings.NewPassword + if cacheErr == nil { + userData.Password = securitySettings.NewPassword + } } - if securitySettings.Twofa == "on" { + if securitySettings.Twofa == constants.On { update["$set"].(bson.M)["security.mfa"] = true - } else if securitySettings.Twofa == "off" { + if cacheErr == nil { + userData.Security.MFA = true + } + } else if securitySettings.Twofa == constants.Off { update["$set"].(bson.M)["security.mfa"] = false + if cacheErr == nil { + userData.Security.MFA = false + } + } + + _, err := collection.UpdateOne(ctx, filter, update) + if err != nil { + logrus.Error(err) + return err + } + + if cacheErr == nil { + cache.SetUser(appsession, userData) + } + + return nil +} + +func GetNotificationSettings(ctx *gin.Context, appsession *models.AppSession, email string) (models.NotificationsRequest, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return models.NotificationsRequest{}, errors.New("database is nil") + } + + // check if user is in cache + if userData, err := cache.GetUser(appsession, email); err == nil { + var bookingReminder string + if userData.Notifications.BookingReminder { + bookingReminder = constants.On + } else { + bookingReminder = constants.Off + } + + var invites string + if userData.Notifications.Invites { + invites = constants.On + } else { + invites = constants.Off + } + + return models.NotificationsRequest{ + Email: userData.Email, + Invites: invites, + BookingReminder: bookingReminder, + }, 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.Error(err) + return models.NotificationsRequest{}, err + } + + // Add the user to the cache if cache is not nil + cache.SetUser(appsession, user) + + var bookingReminder string + if user.Notifications.BookingReminder { + bookingReminder = constants.On + } else { + bookingReminder = constants.Off + } + + var invites string + if user.Notifications.Invites { + invites = constants.On + } else { + invites = constants.Off + } + + return models.NotificationsRequest{ + Email: user.Email, + Invites: invites, + BookingReminder: bookingReminder, + }, nil +} + +func UpdateNotificationSettings(ctx *gin.Context, appsession *models.AppSession, notificationSettings models.NotificationsRequest) 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, notificationSettings.Email) + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") + + filter := bson.M{"email": notificationSettings.Email} + update := bson.M{"$set": bson.M{}} + + if notificationSettings.Invites == constants.On { + update["$set"].(bson.M)["notifications.invites"] = true + if cacheErr == nil { + userData.Notifications.Invites = true + } + } else if notificationSettings.Invites == constants.Off { + update["$set"].(bson.M)["notifications.invites"] = false + if cacheErr == nil { + userData.Notifications.Invites = false + } + } + + if notificationSettings.BookingReminder == constants.On { + update["$set"].(bson.M)["notifications.bookingReminder"] = true + if cacheErr == nil { + userData.Notifications.BookingReminder = true + } + } else if notificationSettings.BookingReminder == constants.Off { + update["$set"].(bson.M)["notifications.bookingReminder"] = false + if cacheErr == nil { + userData.Notifications.BookingReminder = false + } } _, err := collection.UpdateOne(ctx, filter, update) @@ -1119,5 +1227,10 @@ func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession, sec return err } + // update user in cache + if cacheErr == nil { + cache.SetUser(appsession, userData) + } + return nil } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 895e3b5a..ce8a7712 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -147,25 +147,6 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } -// ViewBookings handles the retrieval of all bookings for a user -func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { - // Extract the email query parameter - email := ctx.Query("email") - if email == "" || !utils.ValidateEmail(email) { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil)) - return - } - - // Get all bookings for the userBooking - bookings, err := database.GetUserBookings(ctx, appsession, email) - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusNotFound, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil)) - return - } - - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched bookings!", bookings)) -} - func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { var cancelRequest map[string]interface{} if err := ctx.ShouldBindJSON(&cancelRequest); err != nil { @@ -251,42 +232,44 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { } func GetUserDetails(ctx *gin.Context, appsession *models.AppSession) { - // Extract the email query parameter - email := ctx.Query("email") - if email == "" || !utils.ValidateEmail(email) { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil)) - return - } - - if !database.EmailExists(ctx, appsession, email) { - ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "User not found", constants.InternalServerErrorCode, "User not found", nil)) - return + var request models.RequestEmail + if err := ctx.ShouldBindJSON(&request); err != nil { + emailStr := ctx.Query("email") + if emailStr == "" { + 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 + } + request.Email = emailStr } // Get all the user details - user, err := database.GetUserDetails(ctx, appsession, email) + user, err := database.GetUserDetails(ctx, appsession, request.Email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusNotFound, "Failed to get user details", constants.InternalServerErrorCode, "Failed to get user details", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get user details", constants.InternalServerErrorCode, "Failed to get user details", nil)) return } ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched user details!", user)) } + func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession) { - var user models.UserDetails + var user models.UserDetailsRequest if err := ctx.ShouldBindJSON(&user); err != nil { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid JSON payload", nil)) return } - // Check if the user exists - if !database.EmailExists(ctx, appsession, user.Email) { - ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "User not found", constants.InternalServerErrorCode, "User not found", nil)) - return - } - var err error // Update the user details in the database - _, err = database.UpdateUserDetails(ctx, appsession, user) + _, err := database.UpdateUserDetails(ctx, appsession, user) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to update user details", constants.InternalServerErrorCode, "Failed to update user details", nil)) return @@ -295,33 +278,6 @@ func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully updated user details!", nil)) } -func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { - // Fetch floor number from query parameters - floorNo := ctx.Query("floorNo") - if floorNo == "" { - floorNo = "0" - } - - var rooms []models.Room - rooms, err := database.GetAllRooms(ctx, appsession, floorNo) - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get rooms", constants.InternalServerErrorCode, "Failed to get rooms", nil)) - return - } - - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched rooms!", rooms)) -} - -func GetUsers(ctx *gin.Context, appsession *models.AppSession) { - users, err := database.GetAllUsers(ctx, appsession) - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get users", constants.InternalServerErrorCode, "Failed to get users", nil)) - return - } - - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched users!", users)) -} - // Helper function to handle validation of requests func HandleValidationErrors(ctx *gin.Context, err error) { var ve validator.ValidationErrors @@ -414,6 +370,33 @@ func FilterCollection(ctx *gin.Context, appsession *models.AppSession, collectio Sort: sanitizedSort, } + if collectionName == "RoomBooking" { + // check that the .Filter in filter has the email key set and that the value is the same as the email in the appsession otherwise set the email key to the email in the appsession + if filter.Filter["email"] == nil || filter.Filter["email"] == "" { + email, err := AttemptToGetEmail(ctx, appsession) + + if err != nil { + logrus.Error("Failed to get email because: ", err) + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + filter.Filter["email"] = email + } else { + email, err := AttemptToGetEmail(ctx, appsession) + + if err != nil { + logrus.Error("Failed to get email because: ", err) + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + if filter.Filter["email"] != email { + filter.Filter["email"] = email + } + } + } + res, totalResults, err := database.FilterCollectionWithProjection(ctx, appsession, collectionName, filter) if err != nil { @@ -422,26 +405,11 @@ func FilterCollection(ctx *gin.Context, appsession *models.AppSession, collectio return } - if collectionName != "Notifications" { - ctx.JSON(http.StatusOK, utils.SuccessResponseWithMeta(http.StatusOK, "success", res, gin.H{ - "totalResults": len(res), "totalPages": (totalResults + limit - 1) / limit, "currentPage": page})) - return - } - - // get the users email from the session - if utils.IsSessionSet(ctx) { - email, _ := utils.GetSession(ctx) - err := database.ReadNotifications(ctx, appsession, email) - - if err != nil { - logrus.Error("Failed to read notifications because: ", err) - // it's not a critical error so we don't return an error response - } - } else { - claims, err := utils.GetClaimsFromCTX(ctx) + if collectionName == "Notifications" { + email, err := AttemptToGetEmail(ctx, appsession) if err == nil { - err := database.ReadNotifications(ctx, appsession, claims.Email) + err := database.ReadNotifications(ctx, appsession, email) if err != nil { logrus.Error("Failed to read notifications because: ", err) @@ -503,6 +471,25 @@ func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession) { return } + // check if the email is set + if securitySettings.Email == "" { + // get the email from the appsession + email, err := AttemptToGetEmail(ctx, appsession) + + if err != nil { + logrus.Error("Failed to get email because: ", err) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Email must be provided", + nil)) + return + } + + securitySettings.Email = email + } + // check that if current password is provided, new password and new password confirm are also provided and vice versa if (securitySettings.CurrentPassword == "" && (securitySettings.NewPassword != "" || securitySettings.NewPasswordConfirm != "")) || (securitySettings.NewPassword == "" && (securitySettings.CurrentPassword != "" || securitySettings.NewPasswordConfirm != "")) || @@ -549,3 +536,109 @@ func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully updated security settings!", nil)) } + +func GetSecuritySettings(ctx *gin.Context, appsession *models.AppSession) { + var request models.RequestEmail + if err := ctx.ShouldBindJSON(&request); err != nil { + emailStr := ctx.Query("email") + if emailStr == "" { + 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 + } + request.Email = emailStr + } + + securitySettings, err := database.GetSecuritySettings(ctx, appsession, request.Email) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( + http.StatusInternalServerError, + "Failed to get security settings", + constants.InternalServerErrorCode, + "Failed to get security settings", + nil)) + return + } + + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched security settings!", securitySettings)) +} + +func UpdateNotificationSettings(ctx *gin.Context, appsession *models.AppSession) { + var notificationsSettings models.NotificationsRequest + if err := ctx.ShouldBindJSON(¬ificationsSettings); err != nil { + email, err := AttemptToGetEmail(ctx, appsession) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid request payload", + constants.InvalidRequestPayloadCode, + "Invalid JSON payload", + nil)) + return + } + notificationsSettings.Email = email + + invitesStr := ctx.Query("invites") + if invitesStr != "" { + notificationsSettings.Invites = invitesStr + } + + bookingReminderStr := ctx.Query("bookingReminder") + if bookingReminderStr != "" { + notificationsSettings.BookingReminder = bookingReminderStr + } + } + + // update the notification settings + if err := database.UpdateNotificationSettings(ctx, appsession, notificationsSettings); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( + http.StatusInternalServerError, + "Failed to update notification settings", + constants.InternalServerErrorCode, + "Failed to update notification settings", + nil)) + return + } +} + +func GetNotificationSettings(ctx *gin.Context, appsession *models.AppSession) { + var request models.RequestEmail + if err := ctx.ShouldBindJSON(&request); err != nil { + emailStr := ctx.Query("email") + if emailStr == "" { + 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 + } + request.Email = emailStr + } + + notificationSettings, err := database.GetNotificationSettings(ctx, appsession, request.Email) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( + http.StatusInternalServerError, + "Failed to get notification settings", + constants.InternalServerErrorCode, + "Failed to get notification settings", + nil)) + return + } + + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched notification settings!", notificationSettings)) +} diff --git a/occupi-backend/pkg/handlers/auth_helpers.go b/occupi-backend/pkg/handlers/auth_helpers.go index 4e50bc1e..8fb42125 100644 --- a/occupi-backend/pkg/handlers/auth_helpers.go +++ b/occupi-backend/pkg/handlers/auth_helpers.go @@ -428,3 +428,17 @@ func AllocateAuthTokens(ctx *gin.Context, token string, expirationTime time.Time )) } } + +func AttemptToGetEmail(ctx *gin.Context, appsession *models.AppSession) (string, error) { + // get the users email from the session + if utils.IsSessionSet(ctx) { + email, _ := utils.GetSession(ctx) + return email, nil + } else { + claims, err := utils.GetClaimsFromCTX(ctx) + if err != nil { + return "", err + } + return claims.Email, nil + } +} diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 43bd145c..0266bcdc 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -65,8 +65,8 @@ type Notifications struct { } type Security struct { - MFA *bool `json:"mfa" bson:"mfa"` - Biometrics *bool `json:"biometrics" bson:"biometrics"` + MFA bool `json:"mfa" bson:"mfa"` + Biometrics bool `json:"biometrics" bson:"biometrics"` } type Location struct { @@ -89,6 +89,7 @@ type Booking struct { Start time.Time `json:"start" bson:"start" binding:"required"` End time.Time `json:"end" bson:"end" binding:"required"` } + type Cancel struct { BookingID string `json:"bookingId" bson:"bookingId" binding:"required"` RoomID string `json:"roomId" bson:"roomId" binding:"required"` diff --git a/occupi-backend/pkg/models/request.go b/occupi-backend/pkg/models/request.go index abb5fbb4..347aa757 100644 --- a/occupi-backend/pkg/models/request.go +++ b/occupi-backend/pkg/models/request.go @@ -57,9 +57,30 @@ type RequestEmails struct { } type SecuritySettingsRequest struct { - Email string `json:"email" binding:"required,email"` + Email string `json:"email" binding:"omitempty,email"` Twofa string `json:"2fa"` CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` NewPasswordConfirm string `json:"newPasswordConfirm"` } + +type UserDetailsRequest struct { + Email string `json:"email" binding:"required,email"` + Name string `json:"name"` + Dob string `json:"dob"` + Gender string `json:"gender"` + SessionEmail string `json:"session_email" binding:"omitempty,email"` + Employeeid string `json:"employeeid" binding:"omitempty,startswith=OCCUPI"` + Number string `json:"number"` + Pronouns string `json:"pronouns"` +} + +type NotificationsRequest struct { + Email string `json:"email" binding:"required,email"` + Invites string `json:"invites"` + BookingReminder string `json:"bookingReminder"` +} + +type ImageUploadRequest struct { + Image string `json:"image" binding:"required"` +} diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index adf37dbe..6c3fd749 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -37,14 +37,17 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { api.POST("/book-room", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.BookRoom(ctx, appsession) }) api.POST("/check-in", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.CheckIn(ctx, appsession) }) api.POST("/cancel-booking", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.CancelBooking(ctx, appsession) }) - api.GET(("/view-bookings"), middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.ViewBookings(ctx, appsession) }) - api.GET("/view-rooms", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.ViewRooms(ctx, appsession) }) + api.GET("/view-bookings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.FilterCollection(ctx, appsession, "RoomBooking") }) + api.GET("/view-rooms", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.FilterCollection(ctx, appsession, "Rooms") }) api.GET("/user-details", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetUserDetails(ctx, appsession) }) - api.PUT("/update-user", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.UpdateUserDetails(ctx, appsession) }) + api.POST("/update-user", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.UpdateUserDetails(ctx, appsession) }) api.GET("/get-users", middleware.ProtectedRoute, middleware.AdminRoute, func(ctx *gin.Context) { handlers.FilterCollection(ctx, appsession, "Users") }) api.GET("/get-push-tokens", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetPushTokens(ctx, appsession) }) api.GET("/get-notifications", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.FilterCollection(ctx, appsession, "Notifications") }) - api.POST("/update-security-settings", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.UpdateSecuritySettings(ctx, appsession) }) + api.POST("/update-security-settings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.UpdateSecuritySettings(ctx, appsession) }) + api.GET("/update-notification-settings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.UpdateSecuritySettings(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) }) } auth := router.Group("/auth") { diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 507eef1d..7cec8218 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -28,6 +28,7 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/router" ) +/* // Tests the ViewBookings handler func TestViewBookingsHandler(t *testing.T) { // set gin run mode @@ -78,7 +79,7 @@ func TestViewBookingsHandler(t *testing.T) { name: "Valid Request", email: "test.example@gmail.com", expectedStatusCode: float64(http.StatusOK), - expectedMessage: "Successfully fetched bookings!", + expectedMessage: "success", expectedBookings: 2, }, { @@ -130,7 +131,7 @@ func TestViewBookingsHandler(t *testing.T) { assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") }) } -} +}*/ type testCase struct { name string From 8349383bd9a196476e0fd13e7b5cae5cca7a490a Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Fri, 19 Jul 2024 21:57:43 +0200 Subject: [PATCH 2/2] chore: Update API documentation with new endpoints and examples --- .../pages/api-documentation/api-usage.mdx | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx index a310d9f6..3a7b614c 100644 --- a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx +++ b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx @@ -12,14 +12,20 @@ The API also allows you to retrieve information about these resources. - [Base URL](#base-url) - [Api](#api) - [Resources](#resources) - - [BookRoom](#BookRoom) - - [ViewBookings](#ViewBookings) - - [CancelBooking](#CancelBooking) - - [CheckIn](#CheckIn) + - [Book Room](#BookRoom) + - [View Bookings](#ViewBookings) + - [View Rooms](#ViewRooms) + - [Cancel Booking](#CancelBooking) + - [Check In](#CheckIn) - [Get Users](#Get-users) - [Get Notifications](#Get-notifications) - [Get push tokens](#Get-push-tokens) - [UpdateSecuritySettings](#UpdateSecuritySettings) + - [Get User Details](#GetUserDetails) + - [Update User Details](#UpdateUserDetails) + - [Get Security Settings](#GetSecuritySettings) + - [Update Notification Settings](#UpdateNotificationSettings) + - [Get Notification Settings](#GetNotificationSettings) ## Api @@ -127,8 +133,63 @@ If there are any errors during the process, appropriate error messages are retur - **Content** ```json copy -{ - "emails": "string" +{ + "operator": "eq", // eq, ne, gt, gte, lt, lte, in, nin default to eq + "filter": { + "email": "test@example.com" // the email is required + }, + "order_asc": "creator", // column to sort in ascending order + "order_desc": "floor", // column to sort in descending order + "projection": ["floor"], // this is which columns you want returned + "limit": 50, // default is 50 and is the maximum + "page": 1 // default is 1, but can be incremented to get the next page +} +``` + +**Success Response** + +- **Code:** 200 +- **Content:** `{ "status": 200, "message": "Successfully fetched bookings!", "data": null }` + +**Error Response** + +- **Code:** 400 +- **Content:** `{ "status": 400, "message": "Invalid request payload", "error": {"code":"BAD_REQUEST","details":null,"message":"Expected Email Address"}, }` + + +**Error Response** + +- **Code:** 500 +- **Content:** `{ "status": 500, "message": "Failed to get bookings", "error": {"code":"INTERNAL_SERVER_ERROR","details":"Failed to get bookings","message":"Failed to get bookings"} }` + +### View Rooms + +This endpoint is used to view all rooms in the Occupi system. +Upon a successful request, a list of all rooms is returned. + +- **URL** + + `/api/view-rooms` + +- **Method** + + `GET` + +- **Request Body** + +- **Content** + +```json copy +{ + "operator": "eq", // eq, ne, gt, gte, lt, lte, in, nin default to eq + "filter": { + "floorNo": 3 // this is a filter to filter by + }, + "order_asc": "description", // column to sort in ascending order + "order_desc": "floor", // column to sort in descending order + "projection": ["floor"], // this is which columns you want returned + "limit": 50, // default is 50 and is the maximum + "page": 1 // default is 1, but can be incremented to get the next page } ```