Lots of work on user password reset, #1456
This commit is contained in:
+1
-1
@@ -46,7 +46,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/user/password/reset", Index)
|
r.Get("/user/password/reset", Index)
|
||||||
|
|
||||||
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
|
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
|
// dashboard snapshots
|
||||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||||
|
|||||||
@@ -31,3 +31,9 @@ type AdminUserListItem struct {
|
|||||||
type SendResetPasswordEmailForm struct {
|
type SendResetPasswordEmailForm struct {
|
||||||
UserOrEmail string `json:"userOrEmail" binding:"Required"`
|
UserOrEmail string `json:"userOrEmail" binding:"Required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetUserPasswordForm struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
NewPassword string `json:"newPassword"`
|
||||||
|
ConfirmPassword string `json:"confirmPassword"`
|
||||||
|
}
|
||||||
|
|||||||
+24
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response {
|
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")
|
return ApiSuccess("Email sent")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewResetPasswordForm(c *middleware.Context) Response {
|
func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response {
|
||||||
return ApiSuccess("Email sent")
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
|
||||||
|
|
||||||
type SendEmailCommand struct {
|
type SendEmailCommand struct {
|
||||||
To []string
|
To []string
|
||||||
From string
|
From string
|
||||||
@@ -13,6 +17,11 @@ type SendResetPasswordEmailCommand struct {
|
|||||||
User *User
|
User *User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValidateResetPasswordCodeQuery struct {
|
||||||
|
Code string
|
||||||
|
Result *User
|
||||||
|
}
|
||||||
|
|
||||||
// create mail content
|
// create mail content
|
||||||
func (m *SendEmailCommand) Content() string {
|
func (m *SendEmailCommand) Content() string {
|
||||||
contentType := "text/html; charset=UTF-8"
|
contentType := "text/html; charset=UTF-8"
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const timeLimitCodeLength = 12 + 6 + 40
|
||||||
|
|
||||||
// create a time limit code
|
// create a time limit code
|
||||||
// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
|
// 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"
|
format := "200601021504"
|
||||||
|
|
||||||
var start, end time.Time
|
var start, end time.Time
|
||||||
@@ -42,11 +45,14 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verify time limit code
|
// 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 {
|
if len(code) <= 18 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
minutes := setting.EmailCodeValidMinutes
|
||||||
|
code = code[:timeLimitCodeLength]
|
||||||
|
|
||||||
// split code
|
// split code
|
||||||
start := code[:12]
|
start := code[:12]
|
||||||
lives := code[12:18]
|
lives := code[12:18]
|
||||||
@@ -55,7 +61,9 @@ func VerifyTimeLimitCode(data string, minutes int, code string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// right active code
|
// 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 {
|
if retCode == code && minutes > 0 {
|
||||||
// check time is expired or not
|
// check time is expired or not
|
||||||
before, _ := time.ParseInLocation("200601021504", start, time.Local)
|
before, _ := time.ParseInLocation("200601021504", start, time.Local)
|
||||||
@@ -67,3 +75,24 @@ func VerifyTimeLimitCode(data string, minutes int, code string) bool {
|
|||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,12 +2,10 @@ package notifications
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -19,6 +17,7 @@ var tmplResetPassword = "reset_password.html"
|
|||||||
|
|
||||||
func Init() error {
|
func Init() error {
|
||||||
bus.AddHandler("email", sendResetPasswordEmail)
|
bus.AddHandler("email", sendResetPasswordEmail)
|
||||||
|
bus.AddHandler("email", validateResetPasswordCode)
|
||||||
|
|
||||||
mailTemplates = template.New("name")
|
mailTemplates = template.New("name")
|
||||||
mailTemplates.Funcs(template.FuncMap{
|
mailTemplates.Funcs(template.FuncMap{
|
||||||
@@ -55,7 +54,7 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
var data = getMailTmplData(cmd.User)
|
var data = getMailTmplData(cmd.User)
|
||||||
code := CreateUserActiveCode(cmd.User, nil)
|
code := createUserEmailCode(cmd.User, nil)
|
||||||
data["Code"] = code
|
data["Code"] = code
|
||||||
|
|
||||||
mailTemplates.ExecuteTemplate(&buffer, tmplResetPassword, data)
|
mailTemplates.ExecuteTemplate(&buffer, tmplResetPassword, data)
|
||||||
@@ -70,44 +69,21 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUserActiveCode(u *m.User, startInf interface{}) string {
|
func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
|
||||||
minutes := setting.EmailCodeValidMinutes
|
login := getLoginForEmailCode(query.Code)
|
||||||
data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands
|
if login == "" {
|
||||||
code := CreateTimeLimitCode(data, minutes, startInf)
|
return m.ErrInvalidEmailCode
|
||||||
|
}
|
||||||
|
|
||||||
// add tail hex username
|
userQuery := m.GetUserByLoginQuery{LoginOrEmail: login}
|
||||||
code += hex.EncodeToString([]byte(u.Login))
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
return code
|
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
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ function (angular) {
|
|||||||
$scope.formModel = {};
|
$scope.formModel = {};
|
||||||
$scope.mode = 'send';
|
$scope.mode = 'send';
|
||||||
|
|
||||||
if ($location.search().code) {
|
var params = $location.search();
|
||||||
|
if (params.code) {
|
||||||
$scope.mode = 'reset';
|
$scope.mode = 'reset';
|
||||||
|
$scope.formModel.code = params.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.sendResetEmail = function() {
|
$scope.sendResetEmail = function() {
|
||||||
@@ -33,7 +35,7 @@ function (angular) {
|
|||||||
return;
|
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');
|
$location.path('login');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user