From 1468866a85e76e14f43677ac0ad34e7b0d43d828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Schro=CC=88ter?= Date: Sun, 4 Oct 2020 04:17:26 +0200 Subject: [PATCH] Add endpoint to send email for the username and a link to reset password --- appapi.yaml | 29 ++++++++++++++ assets/email/forgotPassword.tmpl | 37 ++++++++++++++++++ endpoint/user/endpoint.go | 66 ++++++++++++++++++++++++++++++++ endpoint/user/request.go | 5 +++ setup/setup.go | 15 ++++++++ 5 files changed, 152 insertions(+) create mode 100644 assets/email/forgotPassword.tmpl diff --git a/appapi.yaml b/appapi.yaml index 9c42192..77c4c02 100755 --- a/appapi.yaml +++ b/appapi.yaml @@ -307,6 +307,35 @@ paths: schema: $ref: '#/components/schemas/internal-error' + '/forgotpassword': + post: + summary: Create a reset password request. + description: Called when a user forgot the account's username or password. The user will receive an email with the account's username and a link to change the password without knowing the old password. + requestBody: + description: Either the user's username or the user's email address. + content: + application/json: + schema: + oneOf: + - type: object + required: + - email + properties: + email: + type: string + format: email + - type: object + required: + - username + properties: + username: + type: string + responses: + '200': + description: The email with the username and password reset link has been sent. + '404': + description: There is no user with the given email address. + '/refreshjwt': patch: summary: Get a new jwt token. diff --git a/assets/email/forgotPassword.tmpl b/assets/email/forgotPassword.tmpl new file mode 100644 index 0000000..bb38dcc --- /dev/null +++ b/assets/email/forgotPassword.tmpl @@ -0,0 +1,37 @@ +
+ +
+ + Your Request for a Password Reset! + +

Hello, {{.Username}}!

+

You requested credentials for your Akamu account.

+

+ Your Username: {{.Username}} +

+

+ To reset your password, click the following link and set a new password: +

+

+ https://dev.akamu.de/api/v2/forgotpassword/reset?token={{.Token}} +

+ +

Note that the link is only valid for 24 hours.

+ +

If you did not request a password reset, just ignore this email.

+ +

Have fun!
Akamu

+
+
diff --git a/endpoint/user/endpoint.go b/endpoint/user/endpoint.go index 33a678b..107c563 100755 --- a/endpoint/user/endpoint.go +++ b/endpoint/user/endpoint.go @@ -327,6 +327,51 @@ func getAllUsers(repository UserQuery) gin.HandlerFunc { } } +func ForgotPassword(repository UserQuery, tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail) gin.HandlerFunc { + return func(ctx *gin.Context) { + var req ForgotPasswordRequest + if err := ctx.BindJSON(&req); err != nil { + ctx.AbortWithError(http.StatusBadRequest, err) + return + } + + var user *schemas.CredentialsSchema + var err error + + if len(req.Email) > 0 { + // Validate email address + user, err = repository.SelectCredentialsByField(FieldEmail, req.Email) + } else if len(req.Username) > 0 { + // Validate username + user, err = repository.SelectCredentialsByField(FieldUsername, req.Username) + } else { + raven.CaptureMessage(fmt.Sprintf("did bind invalid ForgotPassword request: %+v", req), nil) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + + // This is the error from one of the SelectCredentialsByField calls + if err != nil { + if err == sql.ErrNoRows { + ctx.AbortWithStatus(http.StatusNotFound) + return + } + + raven.CaptureError(err, nil) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + if err := sendPasswordResetEmail(tokenFactory, templatemail, user.ID, user.Username, user.Email, user.PasswordCipher); err != nil { + raven.CaptureError(err, nil) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) + } +} + func sendRegistrationVerification(tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail, userID uint32, username, email string) error { // Create token token, err := tokenFactory.CreateConfirmRegistrationToken(userID, email) @@ -347,3 +392,24 @@ func sendRegistrationVerification(tokenFactory otp.TokenFactory, templatemail *s return nil } + +func sendPasswordResetEmail(tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail, userID uint32, username, email, hash string) error { + // Create token + token, err := tokenFactory.CreatePasswordResetToken(userID, hash) + if err != nil { + return fmt.Errorf("failed to create token: %v", err) + } + + // Send email + if err = templatemail.Send(mail.Address{Name: "username", Address: email}, struct { + Username string + Token string + }{ + Username: username, + Token: token, + }); err != nil { + return fmt.Errorf("failed to send email: %v", err) + } + + return nil +} diff --git a/endpoint/user/request.go b/endpoint/user/request.go index f7d46ed..7f46289 100644 --- a/endpoint/user/request.go +++ b/endpoint/user/request.go @@ -17,3 +17,8 @@ type PatchUserRequest struct { type PatchPasswordRequest struct { Password string `json:"password" binding:"required"` } + +type ForgotPasswordRequest struct { + Email string `json:"email" binding:"required_without=Username,omitempty,email"` + Username string `json:"username" binding:"required_without=Email"` +} diff --git a/setup/setup.go b/setup/setup.go index 5331ce8..24abeca 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -242,6 +242,16 @@ func SetupRoutes(courseRepository course.CourseQuery, flashcardRepository flashc BodyFiles: []string{"assets/email/registrationVerification.tmpl"}, } + forgotPasswordMail := &sendmail.TemplateMail{ + Transport: smtpTransport, + From: mail.Address{ + Name: "Akamu", + Address: "noreply@akamu.de", + }, + Subject: "Your Request for a Password Reset!", + BodyFiles: []string{"assets/email/forgotPassword.tmpl"}, + } + //Middleware router.Use(setCORS()) @@ -264,6 +274,11 @@ func SetupRoutes(courseRepository course.CourseQuery, flashcardRepository flashc ctx.Status(200) }) + router.POST("/forgotpassword", user.ForgotPassword(userRepository, tokenFactory, forgotPasswordMail)) + router.OPTIONS("/forgotpassword", func(ctx *gin.Context) { + ctx.Header("Allow", "POST") + ctx.Status(200) + }) router.PATCH("/refreshjwt", authMiddleware.RefreshHandler) router.OPTIONS("/refreshjwt", func(ctx *gin.Context) { ctx.Header("Allow", "PATCH") -- GitLab