Skip to content

Commit

Permalink
Merge pull request #148 from COS301-SE-2024/feat/backend/reset-password
Browse files Browse the repository at this point in the history
Feat/backend/reset password
  • Loading branch information
u21631532 authored Jul 8, 2024
2 parents 8c3bea5 + d074cde commit a3fc24d
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 4 deletions.
3 changes: 3 additions & 0 deletions occupi-backend/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
OccupiDomains = "OCCUPI_DOMAINS"
Env = "ENV"
OtpExpiration = "OTP_EXPIRATION"
FrontendURL = "FRONTEND_URL"

)

// init viper
Expand Down Expand Up @@ -239,3 +241,4 @@ func GetOTPExpiration() int {
}
return expiration
}

121 changes: 121 additions & 0 deletions occupi-backend/pkg/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ func DeleteOTP(ctx *gin.Context, appsession *models.AppSession, email string, ot
return true, nil
}

// GetResetOTP retrieves the OTP for the given email and OTP from the database
func GetResetOTP(ctx context.Context, db *mongo.Client, email, otp string) (*models.OTP, error) {
collection := db.Database("Occupi").Collection("OTPs")
var resetOTP models.OTP
filter := bson.M{"email": email, "otp": otp}
err := collection.FindOne(ctx, filter).Decode(&resetOTP)
if err != nil {
return nil, err
}
return &resetOTP, nil
}


// verifies a user in the database
func VerifyUser(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) {
// Verify the user in the database and set next date to verify to 30 days from now
Expand Down Expand Up @@ -502,3 +515,111 @@ func CheckIfUserIsAdmin(ctx *gin.Context, appsession *models.AppSession, email s
}
return user.Role == constants.Admin, nil
}


// AddResetToken adds a reset token to the database
func AddResetToken(ctx context.Context, db *mongo.Client, email string, resetToken string, expirationTime time.Time) (bool, error) {
collection := db.Database("Occupi").Collection("ResetTokens")
resetTokenStruct := models.ResetToken{
Email: email,
Token: resetToken,
ExpireWhen: expirationTime,
}
_, err := collection.InsertOne(ctx, resetTokenStruct)
if err != nil {
logrus.Error(err)
return false, err
}
return true, nil
}

// retrieves the email associated with a reset token
func GetEmailByResetToken(ctx context.Context, db *mongo.Client, resetToken string) (string, error) {
collection := db.Database("Occupi").Collection("ResetTokens")
filter := bson.M{"token": resetToken}
var resetTokenStruct models.ResetToken
err := collection.FindOne(ctx, filter).Decode(&resetTokenStruct)
if err != nil {
logrus.Error(err)
return "", err
}
return resetTokenStruct.Email, nil
}

// CheckResetToken function
func CheckResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) {
// Access the "ResetTokens" collection within the "Occupi" database.
collection := db.Database("Occupi").Collection("ResetTokens")

// Create a filter to find the document matching the provided email and token.
filter := bson.M{"email": email, "token": token}

// Define a variable to hold the reset token document.
var resetToken models.ResetToken

// Attempt to find the document in the collection.
err := collection.FindOne(ctx, filter).Decode(&resetToken)
if err != nil {
// Log and return the error if the document cannot be found or decoded.
logrus.Error(err)
return false, err
}

// Check if the current time is after the token's expiration time.
if time.Now().After(resetToken.ExpireWhen) {
// Return false indicating the token has expired.
return false, nil
}

// Return true indicating the token is still valid.
return true, nil
}

// UpdateUserPassword, which updates the password in the database set by the user
func UpdateUserPassword(ctx *gin.Context, db *mongo.Client, email string, password string) (bool, error) {
// Update the password in the database
collection := db.Database("Occupi").Collection("Users")
filter := bson.M{"email": email}
update := bson.M{"$set": bson.M{"password": password}}
_, err := collection.UpdateOne(ctx, filter,update)
if err != nil {
logrus.Error(err)
return false, err
}
return true, nil
}

// ClearRestToekn, removes the reset token from the database
func ClearResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) {
// Delete the token from the database
collection := db.Database("Occupi").Collection("ResetTokens")
filter := bson.M{"email": email, "token": token}
_, err := collection.DeleteOne(ctx,filter)
if err != nil {
logrus.Error(err)
return false, err
}
return true, nil
}

// ValidateResetToken
func ValidateResetToken(ctx context.Context, db *mongo.Client, email, token string) (bool, string, error) {
// Find the reset token document
var resetToken models.ResetToken
collection := db.Database("Occupi").Collection("ResetTokens")
err := collection.FindOne(ctx, bson.M{"email": email, "token": token}).Decode(&resetToken)
if err != nil {
if err == mongo.ErrNoDocuments {
return false, "Invalid or expired token", nil
}
return false, "", err
}

// Check if the token has expired
if time.Now().After(resetToken.ExpireWhen) {
return false, "Token has expired", nil
}

return true, "", nil
}

98 changes: 96 additions & 2 deletions occupi-backend/pkg/handlers/auth_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,106 @@ func ReverifyUsersEmail(ctx *gin.Context, appsession *models.AppSession, email s
"Please check your email for the OTP to re-verify your account.",
nil))
}
// common handler logic for reset
func handlePasswordReset(ctx *gin.Context, appsession *models.AppSession, email string) {
// Sanitize and validate email
sanitizedEmail := utils.SanitizeInput(email)
if !utils.ValidateEmail(sanitizedEmail) {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
http.StatusBadRequest,
"Invalid email address",
constants.InvalidRequestPayloadCode,
"Expected a valid format for email address",
nil))
return
}

// Check if the email exists in the database
if exists := database.EmailExists(ctx, appsession, sanitizedEmail); !exists {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
http.StatusBadRequest,
"Email not registered",
constants.InvalidAuthCode,
"Please register first before attempting to reset password",
nil))
return
}

// Generate a OTP for the user to reset their password
otp, err := utils.GenerateOTP()
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error("Failed to generate OTP:", err)
return
}

// Save the OTP in the database
success, err := database.AddOTP(ctx, appsession, sanitizedEmail, otp)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error("Failed to save OTP:", err)
return
}
if !success {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error("Failed to save OTP: operation unsuccessful")
return
}

// Send the email to the user with the OTP
subject := "Password Reset - Your One-Time Password"
body := mail.FormatResetPasswordEmailBody(otp,email)

if err := mail.SendMail(sanitizedEmail, subject, body); err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error("Failed to send email:", err)
return
}

ctx.JSON(http.StatusOK, utils.SuccessResponse(
http.StatusOK,
"Password reset OTP sent to your email",
nil))
}

// handler for reseting a users password TODO: complete implementation
func ResetPassword(ctx *gin.Context, appsession *models.AppSession) {
// this will contain reset password logic
var request struct {
Email string `json:"email" binding:"required,email"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
http.StatusBadRequest,
"Invalid email address",
constants.InvalidRequestPayloadCode,
"Expected a valid format for email address",
nil))
return
}

handlePasswordReset(ctx, appsession, request.Email)
}

func ForgotPassword(ctx *gin.Context, appsession *models.AppSession) {
var request struct {
Email string `json:"email" binding:"required,email"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
http.StatusBadRequest,
"Invalid email address",
constants.InvalidRequestPayloadCode,
"Expected a valid format for email address",
nil))
return
}

handlePasswordReset(ctx, appsession, request.Email)
}





// handler for logging out a user
func Logout(ctx *gin.Context) {
session := sessions.Default(ctx)
Expand Down
17 changes: 17 additions & 0 deletions occupi-backend/pkg/mail/email_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,20 @@ func appendFooter() string {
</html>
`
}


// FormatPasswordResetEmailBody(otp, email)
func FormatResetPasswordEmailBody(otp string, email string) string {
return appendHeader("Password Reset") + `
<div class="content">
<p>Dear ` + email + `,</p>
<p>
You have requested to reset your password. Your One-Time Password (OTP) is:<br>
<h2 style="color: #4a4a4a; background-color: #f0f0f0; padding: 10px; display: inline-block;">` + otp + `</h2><br><br>
Please use this OTP to reset your password. If you did not request this email, please ignore it.<br><br>
This OTP will expire in 10 minutes.<br><br>
Thank you,<br>
<b>The Occupi Team</b><br>
</p>
</div>` + appendFooter()
}
6 changes: 6 additions & 0 deletions occupi-backend/pkg/models/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,9 @@ type Room struct {
type RoomRequest struct {
FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"`
}

type ResetToken struct {
Email string `bson:"email"`
Token string `bson:"token"`
ExpireWhen time.Time `bson:"expireWhen"`
}
1 change: 1 addition & 0 deletions occupi-backend/pkg/models/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type RequestUser struct {
type RequestUserOTP struct {
Email string `json:"email" binding:"required,email"`
OTP string `json:"otp" binding:"required,otp,len=6"`
Password string `json:"password" binding:"required,min=8"`
}

type ErrorMsg struct {
Expand Down
4 changes: 3 additions & 1 deletion occupi-backend/pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func OccupiRouter(router *gin.Engine, db *mongo.Client, cache *bigcache.BigCache
auth.POST("/register", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Register(ctx, appsession) })
auth.POST("/verify-otp", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.VerifyOTP(ctx, appsession) })
auth.POST("/logout", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.Logout(ctx) })
// auth.POST("/reset-password", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.ForgotPassword(ctx) })
// it's typically used by users who can't log in because they've forgotten their password.
auth.POST("/reset-password",middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.ResetPassword(ctx, appsession) })
auth.POST("/forgot-password", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.ForgotPassword(ctx, appsession)})
}
}
Loading

0 comments on commit a3fc24d

Please sign in to comment.