Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/backend/reset password #148

Merged
merged 50 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8fd48d0
chore: Implement email extraction logic in ResetPassword handler
u21631532 Jul 3, 2024
83f495c
Merge branch 'feat/backend/reset-password' of https://github.com/COS3…
u21631532 Jul 3, 2024
ea60a94
chore: Add email existence check in ResetPassword handler
u21631532 Jul 3, 2024
cc4e888
chore: Sanitize and validate email in ResetPassword handler
u21631532 Jul 3, 2024
db5d2ee
chore: Generate and save reset token in ResetPassword handler
u21631532 Jul 3, 2024
6f618d5
chore: Add functions for managing reset tokens
u21631532 Jul 3, 2024
0b8d645
chore: Add ResetToken struct for managing reset tokens
u21631532 Jul 3, 2024
13b054f
chore: added UpdateUserPassword function
u21631532 Jul 3, 2024
b8f78fa
added ClearResetToken function in database
u21631532 Jul 3, 2024
5d78786
added GetEmailByResetToken function
u21631532 Jul 3, 2024
6c0d5c4
chore: Send password reset link to user's email
u21631532 Jul 3, 2024
c0c2ceb
chore: Added validation for new password and token in the database
u21631532 Jul 3, 2024
038ad68
chore: Add email validation for password reset token in CompletePassw…
u21631532 Jul 3, 2024
204f4b2
chore: Update password + reset token in CompletePasswordReset handler
u21631532 Jul 3, 2024
f73adbb
chore: Refactor password reset email formatting and handling
u21631532 Jul 3, 2024
2c1f385
chore: Improve reset token validation and handling in CheckResetToken…
u21631532 Jul 3, 2024
fce03fe
chore: Add GetFrontendURL function to retrieve frontend URL from config
u21631532 Jul 3, 2024
ddfa8fd
chore: Refactor password reset functionality and improve token handling
u21631532 Jul 3, 2024
5900276
chore: Add unit tests for password reset token functionality
u21631532 Jul 3, 2024
be4fed5
chore: Add forgot password functionality and reset password endpoint
u21631532 Jul 3, 2024
af02d3d
Refactor password reset endpoints and improve token handling
u21631532 Jul 4, 2024
0fddad9
chore: Update reset password email subject and link for consistency
u21631532 Jul 4, 2024
f0a15c7
chore: Add FRONTEND_URL configuration variable
u21631532 Jul 4, 2024
bfbd8aa
refactor: Update forgot password endpoint to improve token handling a…
u21631532 Jul 4, 2024
3fd797c
Refactor reset token validation and handling in ValidateResetToken fu…
u21631532 Jul 4, 2024
ececf60
Refactor password reset endpoints and improve token handling
u21631532 Jul 4, 2024
6045176
refactor: Improve password reset token handling and validation
u21631532 Jul 4, 2024
6d46e05
refactor: Add PasswordResetProtectedRoute middleware for password res…
u21631532 Jul 4, 2024
ab10077
refactor: Update forgot password reset endpoint to use PasswordResetP…
u21631532 Jul 4, 2024
94f99bf
fixed linting issue
u21631532 Jul 4, 2024
4bd0db1
refactor: Remove the PasswordResetProtectedRoute, was causing linting…
u21631532 Jul 4, 2024
727bd8b
refactor: Update forgot password reset endpoint to use ProtectedRoute…
u21631532 Jul 4, 2024
38fd255
refactor: Update forgot password reset endpoint to use OTP for passwo…
u21631532 Jul 6, 2024
e2fe879
refactor: Update forgot password reset endpoint to use OTP for passwo…
u21631532 Jul 6, 2024
d2abe3f
refactor: Update password reset email body to include OTP
u21631532 Jul 6, 2024
b81b99e
Merge branch 'develop' into feat/backend/reset-password
waveyboym Jul 6, 2024
bd60b27
chore: Add context import to database_test.go
waveyboym Jul 6, 2024
eec12fd
refactor: Update forgot password reset endpoint to use UnProtectedRou…
u21631532 Jul 8, 2024
e665213
refactor: Add password field to RequestUserOTP struct
u21631532 Jul 8, 2024
5d256a7
refactor: Update password reset email body to include OTP, OTP will e…
u21631532 Jul 8, 2024
c625de1
refactor: Update forgot password reset endpoint to use UnProtectedRou…
u21631532 Jul 8, 2024
90deff4
refactor: Remove GetFrontendURL function and add GetResetOTP function…
u21631532 Jul 8, 2024
159cfad
Merge branch 'feat/backend/reset-password' of https://github.com/COS3…
u21631532 Jul 8, 2024
3855861
refactor: Update forgot password reset endpoint to include OTP for pa…
u21631532 Jul 8, 2024
cc8cf57
refactor: Update reset password reset endpoint to include OTP for pas…
u21631532 Jul 8, 2024
ac2b101
refactor: Update forgot password reset endpoint to include OTP for pa…
u21631532 Jul 8, 2024
de36392
refactor: Update forgot password reset endpoint to include OTP for pa…
u21631532 Jul 8, 2024
1cf4dfe
Merge remote-tracking branch 'origin/develop' into feat/backend/reset…
u21631532 Jul 8, 2024
92f4ae2
refactor: Update forgot password reset endpoint to include OTP for pa…
u21631532 Jul 8, 2024
d074cde
refactor: added a helper function to the forgot password and reset pa…
u21631532 Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions occupi-backend/configs/config.go
waveyboym marked this conversation as resolved.
Show resolved Hide resolved
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
Loading