diff --git a/Featurecreep.md b/Featurecreep.md index 7d55eb95b..826997236 100644 --- a/Featurecreep.md +++ b/Featurecreep.md @@ -233,7 +233,7 @@ Teams sind global, d.h. Ein Team kann mehrere Namespaces verwalten. -> Soweit es geht und Sinnvoll ist auf den neuen Handler umziehen -> Login/Register/Password-reset geht natürlich nicht -> Bleibt noch Profile abrufen und Einstellungen -> Macht also keinen Sinn das auf den neuen Handler umzuziehen -* [ ] Email-Verifizierung beim Registrieren +* [x] Email-Verifizierung beim Registrieren * [x] Password Reset -> Link via email oder so * [ ] Settings diff --git a/REST-Tests/auth.http b/REST-Tests/auth.http index bb2424d62..79edc7f1f 100644 --- a/REST-Tests/auth.http +++ b/REST-Tests/auth.http @@ -9,17 +9,15 @@ Content-Type: application/json > {% client.global.set("auth_token", response.body.token); %} -### - -## Register +### Register POST http://localhost:8080/api/v1/register Content-Type: application/json { - "username": "user3", + "username": "user4", "password": "1234", - "email": "3@knt.li" + "email": "4@knt.li" } ### diff --git a/REST-Tests/users.http b/REST-Tests/users.http index c6a889154..e8c066faf 100644 --- a/REST-Tests/users.http +++ b/REST-Tests/users.http @@ -30,7 +30,7 @@ Accept: application/json "user_name": "user" } -### Request a password to reset a password +### Request a token to reset a password POST http://localhost:8080/api/v1/user/password/reset Content-Type: application/json Accept: application/json @@ -40,4 +40,14 @@ Accept: application/json "new_password": "1234" } +### Confirm a users email address + +POST http://localhost:8080/api/v1/user/confirm +Content-Type: application/json +Accept: application/json + +{ + "token": "cqucJFxaTBwEZfuQATIBouCvpAfVUWXvrinRXSZpWpxikBMKyBtfNsZysvKOwCPMTsfmHZZeXiHhdBQyAUHFkMiXFAKqzMTWpTTJLkVeoKSkoinlsnxuPiqXjOHJNhnihRtRTdpQARQBlGHBrppojIJwZdKtmXsxwqMDwYKiTuHwjaOKYLeMLQaEWYpmedfvjtwSqhfuitguIatvLbVmtMfEAgwTcHscGeHpPsHFhLMXDqzwCmJYqsXoXxaumMaqaGOTguwvpWXCfvfBSXsjqiTwOcxhdYTRvQNoHijYkzshmrPDwiQcMNyCRzenxaKcrrVPcxJMmMGffjkRQlMtzUyBuHbHLbwQRaadLqPWuKJdXKSjMGiIFzyhCTzOSzMXgSCBtIfRFQaqsUss" +} + ### diff --git a/models/error.go b/models/error.go index 14527111d..27f72934e 100644 --- a/models/error.go +++ b/models/error.go @@ -174,6 +174,29 @@ func IsErrInvalidPasswordResetToken(err error) bool { return ok } +// ErrInvalidEmailConfirmToken is an error where the email confirm token is invalid +type ErrInvalidEmailConfirmToken struct { + Token string +} + +func (err ErrInvalidEmailConfirmToken) Error() string { + return fmt.Sprintf("Invalid email confirm token [Token: %s]", err.Token) +} + +// ErrCodeInvalidEmailConfirmToken holds the unique world-error code of this error +const ErrCodeInvalidEmailConfirmToken = 1010 + +// HTTPError holds the http error description +func (err ErrInvalidEmailConfirmToken) HTTPError() HTTPError { + return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidEmailConfirmToken, Message: "Invalid email confirm token."} +} + +// IsErrInvalidEmailConfirmToken checks if an error is a ErrInvalidEmailConfirmToken. +func IsErrInvalidEmailConfirmToken(err error) bool { + _, ok := err.(ErrInvalidEmailConfirmToken) + return ok +} + // =================== // Empty things errors // =================== diff --git a/models/user.go b/models/user.go index 87eeabca2..cd944b428 100644 --- a/models/user.go +++ b/models/user.go @@ -18,8 +18,10 @@ type User struct { Username string `xorm:"varchar(250) not null unique" json:"username"` Password string `xorm:"varchar(250) not null" json:"-"` Email string `xorm:"varchar(250)" json:"email"` + IsActive bool `json:"-"` PasswordResetToken string `xorm:"varchar(450)" json:"-"` + EmailConfirmToken string `xorm:"varchar(450)" json:"-"` Created int64 `xorm:"created" json:"-"` Updated int64 `xorm:"updated" json:"-"` diff --git a/models/user_add_update.go b/models/user_add_update.go index e712d1982..f80a75022 100644 --- a/models/user_add_update.go +++ b/models/user_add_update.go @@ -1,6 +1,8 @@ package models import ( + "code.vikunja.io/api/models/mail" + "code.vikunja.io/api/models/utils" "golang.org/x/crypto/bcrypt" ) @@ -48,6 +50,12 @@ func CreateUser(user User) (newUser User, err error) { return User{}, err } + // Generate a confirm token + newUser.EmailConfirmToken = utils.MakeRandomString(400) + + // The new user should not be activated until it confirms his mail address + newUser.IsActive = false + // Insert it _, err = x.Insert(newUser) if err != nil { @@ -67,6 +75,13 @@ func CreateUser(user User) (newUser User, err error) { return User{}, err } + // Send the user a mail with a link to confirm the mail + data := map[string]interface{}{ + "User": newUserOut, + } + + mail.SendMailWithTemplate(user.Email, newUserOut.Username+" + Vikunja = <3", "confirm-email", data) + return newUserOut, err } diff --git a/models/user_email_confirm.go b/models/user_email_confirm.go new file mode 100644 index 000000000..a9c177ee0 --- /dev/null +++ b/models/user_email_confirm.go @@ -0,0 +1,25 @@ +package models + +// EmailConfirm holds the token to confirm a mail address +type EmailConfirm struct { + Token string `json:"token"` +} + +// UserEmailConfirm handles the confirmation of an email address +func UserEmailConfirm(c *EmailConfirm) (err error) { + + user := User{} + has, err := x.Where("email_confirm_token = ?", c.Token).Get(&user) + if err != nil { + return + } + + if !has { + return ErrInvalidEmailConfirmToken{Token: c.Token} + } + + user.IsActive = true + user.EmailConfirmToken = "" + _, err = x.Where("id = ?", user.ID).Cols("is_active", "email_confirm_token").Update(&user) + return +} diff --git a/routes/api/v1/swagger/options.go b/routes/api/v1/swagger/options.go index 4f900d9a3..845734808 100644 --- a/routes/api/v1/swagger/options.go +++ b/routes/api/v1/swagger/options.go @@ -46,4 +46,7 @@ type swaggerParameterBodies struct { // in:body PasswordTokenRequest models.PasswordTokenRequest + + // in:body + EmailConfirm models.EmailConfirm } diff --git a/routes/api/v1/user_confirm_email.go b/routes/api/v1/user_confirm_email.go new file mode 100644 index 000000000..03d9108af --- /dev/null +++ b/routes/api/v1/user_confirm_email.go @@ -0,0 +1,46 @@ +package v1 + +import ( + "code.vikunja.io/api/models" + "code.vikunja.io/api/routes/crud" + "github.com/labstack/echo" + "net/http" +) + +// UserConfirmEmail is the handler to confirm a user email +func UserConfirmEmail(c echo.Context) error { + // swagger:operation POST /user/confirm user confirmEmail + // --- + // summary: Confirms a users email address + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EmailConfirm" + // responses: + // "200": + // "$ref": "#/responses/Message" + // "400": + // "$ref": "#/responses/Message" + // "404": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + + // Check for Request Content + var emailConfirm models.EmailConfirm + if err := c.Bind(&emailConfirm); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "No token provided.") + } + + err := models.UserEmailConfirm(&emailConfirm) + if err != nil { + return crud.HandleHTTPError(err) + } + + return c.JSON(http.StatusOK, models.Message{"The email was confirmed successfully."}) +} diff --git a/routes/routes.go b/routes/routes.go index 4efd51c3d..9731a2fa3 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -67,6 +67,7 @@ func RegisterRoutes(e *echo.Echo) { a.POST("/register", apiv1.RegisterUser) a.POST("/user/password/token", apiv1.UserRequestResetPasswordToken) a.POST("/user/password/reset", apiv1.UserResetPassword) + a.POST("/user/confirm", apiv1.UserConfirmEmail) // ===== Routes with Authetification ===== // Authetification diff --git a/templates/mail/confirm-email.html.tmpl b/templates/mail/confirm-email.html.tmpl new file mode 100644 index 000000000..9ac1fbd43 --- /dev/null +++ b/templates/mail/confirm-email.html.tmpl @@ -0,0 +1,16 @@ +{{template "mail-header.tmpl" .}} +

+ Hi {{.User.Username}},
+
+ Welcome to Vikunja! +
+ To confirm you email address, click the link below: +

+ + Confirm your email address + +

+ If the button above doesn't work, copy the url below and paste it in your browsers address bar:
+ {{.FrontendURL}}?userEmailConfirm={{.User.EmailConfirmToken}} +

+{{template "mail-footer.tmpl"}} \ No newline at end of file diff --git a/templates/mail/confirm-email.plain.tmpl b/templates/mail/confirm-email.plain.tmpl new file mode 100644 index 000000000..b15a61e36 --- /dev/null +++ b/templates/mail/confirm-email.plain.tmpl @@ -0,0 +1,7 @@ +Hi {{.User.Username}}, + +Welcome to Vikunja! + +To confirm you email address, click the link below: + +{{.User.EmailConfirmToken}} \ No newline at end of file