diff --git a/assets/web/forgotPassword.tmpl b/assets/web/forgotPassword.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..9dbeb1eea43bf706e3306146980fb9d90a46656b --- /dev/null +++ b/assets/web/forgotPassword.tmpl @@ -0,0 +1,126 @@ + + + + + + + Akamu Account Password Reset + + + + + +
+ {{if .Done}} + Your password reset was successful. + +

You can log in with your new password in the App.

+ {{else if .Valid}} + Set a new password. + + {{range $_, $e := .Errors}} +

{{ $e }}

+ {{ end }} + +
+

+
+ +

+

+
+ +

+ +
+ + {{else if .ExpiredTime}} + Your link has expired. + +

+ Your link has expired since your request is older than 24h.
+ Just create a new request to reset your password. +

+ + {{else if .ExpiredUsed}} + You already updated your password. + +

+ Your link has expired since you already set a new password.
+ If you did not change your password since you requested a password + reset, please contact + support@akamu.de. +

+ + {{else if .Failure}} + An error occurred. + +

+ Please try to reset your password again.
+ If it repeatedly failes, contact the support at + support@akamu.de. +

+ + {{else}} + Your request seems to be invalid. + +

+ Check that you called the link as in your email.
+ Get additional help from + support@akamu.de. +

+ {{ end }} +
+ + diff --git a/endpoint/user/endpoint.go b/endpoint/user/endpoint.go index 107c5637f4e41a2d99dec0f4b40f80692cffe0ad..0e2b25a05010e4a11b55d5b76e349a578cbb12d5 100755 --- a/endpoint/user/endpoint.go +++ b/endpoint/user/endpoint.go @@ -15,6 +15,7 @@ import ( "github.com/getsentry/raven-go" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" "gitlab.akamu.de/akamu/game-server-go/schemas" ) @@ -372,6 +373,88 @@ func ForgotPassword(repository UserQuery, tokenFactory otp.TokenFactory, templat } } +func GetResetPassword(repository UserQuery, tokenFactory otp.TokenFactory) gin.HandlerFunc { + return func(ctx *gin.Context) { + token, ok := ctx.GetQuery("token") + + if !ok { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + // Validate and get payload from token + userID, hash, err := tokenFactory.ParsePasswordResetToken(token) + if err == otp.ErrTokenExpired { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredTime": true}) + return + } else if err != nil { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + // Check if user already updated the password + if cred, err := repository.SelectCredentialsByField(FieldID, userID); err != nil { + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } else if cred.PasswordCipher != hash { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredUsed": true}) + return + } + + ctx.HTML(http.StatusOK, "forgotPassword.tmpl", gin.H{"Valid": true}) + } +} + +func PostResetPassword(repository UserQuery, tokenFactory otp.TokenFactory) gin.HandlerFunc { + return func(ctx *gin.Context) { + token, ok := ctx.GetQuery("token") + + if !ok { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + userID, hash, err := tokenFactory.ParsePasswordResetToken(token) + if err == otp.ErrTokenExpired { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredTime": true}) + return + } else if err != nil { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + var formData struct { + Password string `form:"password" binding:"required,password"` + Confirm string `form:"confirm-password" binding:"required,eqfield=Password"` + } + + if err := ctx.ShouldBind(&formData); err != nil { + errs := err.(validator.ValidationErrors) + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", gin.H{"Valid": true, "Errors": errs.Translate(endpoint.GetTranslator())}) + return + } + + // Check that the password has not been updated since creation of the token + if cred, err := repository.SelectCredentialsByField(FieldID, userID); err != nil { + raven.CaptureError(err, nil) + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } else if cred.PasswordCipher != hash { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredUsed": true}) + return + } + + // Update password + if err := repository.UpdatePassword(userID, formData.Password); err != nil { + raven.CaptureError(err, nil) + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } + + ctx.HTML(http.StatusOK, "forgotPassword.tmpl", gin.H{"Done": true}) + } +} + func sendRegistrationVerification(tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail, userID uint32, username, email string) error { // Create token token, err := tokenFactory.CreateConfirmRegistrationToken(userID, email) diff --git a/setup/setup.go b/setup/setup.go index 24abeca511483c39abad94a9989fd93f13b03bc9..4c56cc6ba00cbccad1080943d9066b0c1054194d 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -279,6 +279,10 @@ func SetupRoutes(courseRepository course.CourseQuery, flashcardRepository flashc ctx.Header("Allow", "POST") ctx.Status(200) }) + + router.GET("/forgotpassword/reset", user.GetResetPassword(userRepository, tokenFactory)) + router.POST("/forgotpassword/reset", user.PostResetPassword(userRepository, tokenFactory)) + router.PATCH("/refreshjwt", authMiddleware.RefreshHandler) router.OPTIONS("/refreshjwt", func(ctx *gin.Context) { ctx.Header("Allow", "PATCH")