diff --git a/pkg/api/api.go b/pkg/api/api.go index 8be5bfe054f..7a96b095348 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -46,7 +46,7 @@ func Register(r *macaron.Macaron) { r.Get("/user/password/reset", Index) r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail)) - r.Post("/api/user/password/reset", wrap(ViewResetPasswordForm)) + r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword)) // dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 4a569f9c492..a30bebf137f 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -31,3 +31,9 @@ type AdminUserListItem struct { type SendResetPasswordEmailForm struct { UserOrEmail string `json:"userOrEmail" binding:"Required"` } + +type ResetUserPasswordForm struct { + Code string `json:"code"` + NewPassword string `json:"newPassword"` + ConfirmPassword string `json:"confirmPassword"` +} diff --git a/pkg/api/password.go b/pkg/api/password.go index 0957e130a61..f3c2b0b7058 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" ) func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response { @@ -22,6 +23,27 @@ func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEm return ApiSuccess("Email sent") } -func ViewResetPasswordForm(c *middleware.Context) Response { - return ApiSuccess("Email sent") +func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response { + query := m.ValidateResetPasswordCodeQuery{Code: form.Code} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrInvalidEmailCode { + return ApiError(400, "Invalid or expired reset password code", nil) + } + return ApiError(500, "Unknown error validating email code", err) + } + + if form.NewPassword != form.ConfirmPassword { + return ApiError(400, "Passwords do not match", nil) + } + + cmd := m.ChangeUserPasswordCommand{} + cmd.UserId = query.Result.Id + cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt) + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to change user password", err) + } + + return ApiSuccess("User password changed") } diff --git a/pkg/models/emails.go b/pkg/models/emails.go index e2ef777a5a6..b7812e756f4 100644 --- a/pkg/models/emails.go +++ b/pkg/models/emails.go @@ -1,5 +1,9 @@ package models +import "errors" + +var ErrInvalidEmailCode = errors.New("Invalid or expired email code") + type SendEmailCommand struct { To []string From string @@ -13,6 +17,11 @@ type SendResetPasswordEmailCommand struct { User *User } +type ValidateResetPasswordCodeQuery struct { + Code string + Result *User +} + // create mail content func (m *SendEmailCommand) Content() string { contentType := "text/html; charset=UTF-8" diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go index 12c9a4c8172..4dbe76c1cad 100644 --- a/pkg/services/notifications/codes.go +++ b/pkg/services/notifications/codes.go @@ -7,12 +7,15 @@ import ( "time" "github.com/Unknwon/com" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) +const timeLimitCodeLength = 12 + 6 + 40 + // create a time limit code // code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string -func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string { +func createTimeLimitCode(data string, minutes int, startInf interface{}) string { format := "200601021504" var start, end time.Time @@ -42,11 +45,14 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string } // verify time limit code -func VerifyTimeLimitCode(data string, minutes int, code string) bool { +func validateUserEmailCode(user *m.User, code string) bool { if len(code) <= 18 { return false } + minutes := setting.EmailCodeValidMinutes + code = code[:timeLimitCodeLength] + // split code start := code[:12] lives := code[12:18] @@ -55,7 +61,9 @@ func VerifyTimeLimitCode(data string, minutes int, code string) bool { } // right active code - retCode := CreateTimeLimitCode(data, minutes, start) + data := com.ToStr(user.Id) + user.Email + user.Login + user.Password + user.Rands + retCode := createTimeLimitCode(data, minutes, start) + fmt.Printf("code : %s\ncode2: %s", retCode, code) if retCode == code && minutes > 0 { // check time is expired or not before, _ := time.ParseInLocation("200601021504", start, time.Local) @@ -67,3 +75,24 @@ func VerifyTimeLimitCode(data string, minutes int, code string) bool { return false } + +func getLoginForEmailCode(code string) string { + if len(code) <= timeLimitCodeLength { + return "" + } + + // use tail hex username query user + hexStr := code[timeLimitCodeLength:] + b, _ := hex.DecodeString(hexStr) + return string(b) +} + +func createUserEmailCode(u *m.User, startInf interface{}) string { + minutes := setting.EmailCodeValidMinutes + data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands + code := createTimeLimitCode(data, minutes, startInf) + + // add tail hex username + code += hex.EncodeToString([]byte(u.Login)) + return code +} diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go new file mode 100644 index 00000000000..be1fc91153b --- /dev/null +++ b/pkg/services/notifications/codes_test.go @@ -0,0 +1,35 @@ +package notifications + +import ( + "testing" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestEmailCodes(t *testing.T) { + + Convey("When generating code", t, func() { + setting.EmailCodeValidMinutes = 120 + + user := &m.User{Id: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"} + code := createUserEmailCode(user, nil) + + Convey("getLoginForCode should return login", func() { + login := getLoginForEmailCode(code) + So(login, ShouldEqual, "asd") + }) + + Convey("Can verify valid code", func() { + So(validateUserEmailCode(user, code), ShouldBeTrue) + }) + + Convey("Cannot verify in-valid code", func() { + code = "ASD" + So(validateUserEmailCode(user, code), ShouldBeFalse) + }) + + }) + +} diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 0475b0704ad..8a162455248 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -2,12 +2,10 @@ package notifications import ( "bytes" - "encoding/hex" "errors" "html/template" "path/filepath" - "github.com/Unknwon/com" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -19,6 +17,7 @@ var tmplResetPassword = "reset_password.html" func Init() error { bus.AddHandler("email", sendResetPasswordEmail) + bus.AddHandler("email", validateResetPasswordCode) mailTemplates = template.New("name") mailTemplates.Funcs(template.FuncMap{ @@ -55,7 +54,7 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { var buffer bytes.Buffer var data = getMailTmplData(cmd.User) - code := CreateUserActiveCode(cmd.User, nil) + code := createUserEmailCode(cmd.User, nil) data["Code"] = code mailTemplates.ExecuteTemplate(&buffer, tmplResetPassword, data) @@ -70,44 +69,21 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { return nil } -func CreateUserActiveCode(u *m.User, startInf interface{}) string { - minutes := setting.EmailCodeValidMinutes - data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands - code := CreateTimeLimitCode(data, minutes, startInf) +func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { + login := getLoginForEmailCode(query.Code) + if login == "" { + return m.ErrInvalidEmailCode + } - // add tail hex username - code += hex.EncodeToString([]byte(u.Login)) - return code + userQuery := m.GetUserByLoginQuery{LoginOrEmail: login} + if err := bus.Dispatch(&userQuery); err != nil { + return err + } + + if !validateUserEmailCode(userQuery.Result, query.Code) { + return m.ErrInvalidEmailCode + } + + query.Result = userQuery.Result + return nil } - -// // verify active code when active account -// func VerifyUserActiveCode(code string) (user *User) { -// minutes := setting.Service.ActiveCodeLives -// -// if user = getVerifyUser(code); user != nil { -// // time limit code -// prefix := code[:base.TimeLimitCodeLength] -// data := com.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands -// -// if base.VerifyTimeLimitCode(data, minutes, prefix) { -// return user -// } -// } -// return nil -// } -// -// // verify active code when active account -// func VerifyUserActiveCode(code string) (user *User) { -// minutes := setting.Service.ActiveCodeLives -// -// if user = getVerifyUser(code); user != nil { -// // time limit code -// prefix := code[:base.TimeLimitCodeLength] -// data := com.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands -// -// if base.VerifyTimeLimitCode(data, minutes, prefix) { -// return user -// } -// } -// return nil -// } diff --git a/public/app/controllers/resetPasswordCtrl.js b/public/app/controllers/resetPasswordCtrl.js index 5d183d891ae..ed693f0d45a 100644 --- a/public/app/controllers/resetPasswordCtrl.js +++ b/public/app/controllers/resetPasswordCtrl.js @@ -12,8 +12,10 @@ function (angular) { $scope.formModel = {}; $scope.mode = 'send'; - if ($location.search().code) { + var params = $location.search(); + if (params.code) { $scope.mode = 'reset'; + $scope.formModel.code = params.code; } $scope.sendResetEmail = function() { @@ -33,7 +35,7 @@ function (angular) { return; } - backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() { $location.path('login'); }); };