Commit 1468866a authored by Julien Schröter's avatar Julien Schröter Committed by Julien Schröter

Add endpoint to send email for the username and a link to reset password

parent 5379acd1
......@@ -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.
......
<div
style="
font: 18px/20px -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol;
width: 80%;
margin: 20px auto;
color: #555;
"
>
<img src="https://akamu.de/img/logo.svg" height="50px" width="64px" />
<div style="width: 100%; margin: 30px auto">
<b style="line-height: inherit; color: #000">
Your Request for a Password Reset!
</b>
<p>Hello, {{.Username}}!</p>
<p>You requested credentials for your Akamu account.</p>
<p>
Your Username: <b>{{.Username}}</b>
</p>
<p>
To reset your password, click the following link and set a new password:
</p>
<p>
<a
href="https://dev.akamu.de/api/v2/forgotpassword/reset?token={{.Token}}"
>https://dev.akamu.de/api/v2/forgotpassword/reset?token={{.Token}}</a
>
</p>
<p>Note that the link is only valid for 24 hours.</p>
<p>If you did not request a password reset, just ignore this email.</p>
<p>Have fun!<br />Akamu</p>
</div>
</div>
......@@ -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
}
......@@ -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"`
}
......@@ -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")
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment