From aa4d60c21ea5997b5c288242b7b51b1106c5a466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 8 Jun 2015 10:57:01 +0200 Subject: [PATCH] Worked on reset password views, refactored out password strength to a reusable directive --- pkg/api/api.go | 7 +++ pkg/api/dtos/user.go | 4 ++ pkg/api/password.go | 27 +++++++++ pkg/models/emails.go | 10 +--- pkg/models/user.go | 10 ++++ pkg/services/mailer/mailer.go | 47 ---------------- pkg/services/notifications/email.go | 4 +- pkg/services/notifications/notifications.go | 38 ++++++++++++- .../notifications/notifications_test.go | 4 +- public/app/controllers/loginCtrl.js | 24 -------- public/app/controllers/resetPasswordCtrl.js | 27 +++++++-- public/app/directives/all.js | 1 + public/app/directives/passwordStrenght.js | 47 ++++++++++++++++ public/app/partials/login.html | 16 +++--- public/app/partials/reset_password.html | 55 ++++++++++++++++++- public/app/routes/all.js | 6 +- public/css/less/login.less | 7 +-- public/emails/reset_password.html | 37 +++++++++++-- 18 files changed, 259 insertions(+), 112 deletions(-) create mode 100644 pkg/api/password.go create mode 100644 public/app/directives/passwordStrenght.js diff --git a/pkg/api/api.go b/pkg/api/api.go index 6ecaa51652e..8be5bfe054f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -41,6 +41,13 @@ func Register(r *macaron.Macaron) { r.Get("/signup", Index) r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp) + // reset password + r.Get("/user/password/send-reset-email", 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/reset", wrap(ViewResetPasswordForm)) + // dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) r.Get("/dashboard/snapshot/*", Index) diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 0224ced599d..4a569f9c492 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -27,3 +27,7 @@ type AdminUserListItem struct { Login string `json:"login"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` } + +type SendResetPasswordEmailForm struct { + UserOrEmail string `json:"userOrEmail" binding:"Required"` +} diff --git a/pkg/api/password.go b/pkg/api/password.go new file mode 100644 index 00000000000..0957e130a61 --- /dev/null +++ b/pkg/api/password.go @@ -0,0 +1,27 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" +) + +func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response { + userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} + + if err := bus.Dispatch(&userQuery); err != nil { + return ApiError(404, "User does not exist", err) + } + + emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result} + if err := bus.Dispatch(&emailCmd); err != nil { + return ApiError(500, "Failed to send email", err) + } + + return ApiSuccess("Email sent") +} + +func ViewResetPasswordForm(c *middleware.Context) Response { + return ApiSuccess("Email sent") +} diff --git a/pkg/models/emails.go b/pkg/models/emails.go index 99c31aa6423..e2ef777a5a6 100644 --- a/pkg/models/emails.go +++ b/pkg/models/emails.go @@ -5,7 +5,6 @@ type SendEmailCommand struct { From string Subject string Body string - Type string Massive bool Info string } @@ -16,13 +15,7 @@ type SendResetPasswordEmailCommand struct { // create mail content func (m *SendEmailCommand) Content() string { - // set mail type - contentType := "text/plain; charset=UTF-8" - if m.Type == "html" { - contentType = "text/html; charset=UTF-8" - } - - // create mail content + contentType := "text/html; charset=UTF-8" content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body return content } @@ -34,6 +27,5 @@ func NewSendEmailCommand(To []string, From, Subject, Body string) SendEmailComma From: From, Subject: Subject, Body: Body, - Type: "html", } } diff --git a/pkg/models/user.go b/pkg/models/user.go index 5efecc8deef..bf697676b32 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -30,6 +30,16 @@ type User struct { Updated time.Time } +func (u *User) NameOrFallback() string { + if u.Name != "" { + return u.Name + } else if u.Login != "" { + return u.Login + } else { + return u.Email + } +} + // --------------------- // COMMANDS diff --git a/pkg/services/mailer/mailer.go b/pkg/services/mailer/mailer.go index 91e57fe9505..c3a1fe3ce63 100644 --- a/pkg/services/mailer/mailer.go +++ b/pkg/services/mailer/mailer.go @@ -55,12 +55,6 @@ func processMailQueue() { } } -func encodeRFC2047(text string) string { - // use mail's rfc2047 to encode any string - addr := mail.Address{Address: text} - return strings.Trim(addr.String(), " <>") -} - func handleEmailCommand(cmd *m.SendEmailCommand) error { log.Info("Sending on queue") mailQueue <- cmd @@ -166,47 +160,6 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error { } return client.Quit() - // smtpServer := "smtp.gmail.com" - // auth := smtp.PlainAuth( - // "", - // "torkel.odegaard@gmail.com", - // "peslpwstnnloiksq", - // smtpServer, - // ) - // - // from := mail.Address{Name: "test", Address: "torkel@test.com"} - // to := mail.Address{Name: "Torkel Ödegaard", Address: "torkel@raintank.io"} - // title := "Message from Grafana" - // - // body := "Testing email sending" - // - // header := make(map[string]string) - // header["From"] = from.String() - // header["To"] = to.String() - // header["Subject"] = encodeRFC2047(title) - // header["MIME-Version"] = "1.0" - // header["Content-Type"] = "text/plain; charset=\"utf-8\"" - // header["Content-Transfer-Encoding"] = "base64" - // - // message := "" - // for k, v := range header { - // message += fmt.Sprintf("%s: %s\r\n", k, v) - // } - // message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body)) - // - // // Connect to the server, authenticate, set the sender and recipient, - // // and send the email all in one step. - // err := smtp.SendMail( - // smtpServer+":587", - // auth, - // from.Address, - // []string{to.Address}, - // []byte(message), - // ) - // if err != nil { - // log.Info("Failed to send email: %v", err) - // } - // kkkk } func buildAndSend(msg *m.SendEmailCommand) (int, error) { diff --git a/pkg/services/notifications/email.go b/pkg/services/notifications/email.go index 22cac1c2301..299b6315f17 100644 --- a/pkg/services/notifications/email.go +++ b/pkg/services/notifications/email.go @@ -20,10 +20,10 @@ func getMailTmplData(u *m.User) map[interface{}]interface{} { data["AppUrl"] = setting.AppUrl data["BuildVersion"] = setting.BuildVersion data["BuildStamp"] = setting.BuildStamp - data["BuildCommit"] = setting.BuildCommit + data["EmailCodeValidHours"] = setting.EmailCodeValidMinutes / 60 data["Subject"] = map[string]interface{}{} if u != nil { - data["User"] = u + data["Name"] = u.NameOrFallback() } return data } diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 702bc0032c3..0475b0704ad 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -35,6 +35,10 @@ func Init() error { return errors.New("Invalid email address for smpt from_adress config") } + if setting.EmailCodeValidMinutes == 0 { + setting.EmailCodeValidMinutes = 120 + } + return nil } @@ -50,7 +54,7 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { var buffer bytes.Buffer - var data = getMailTmplData(nil) + var data = getMailTmplData(cmd.User) code := CreateUserActiveCode(cmd.User, nil) data["Code"] = code @@ -75,3 +79,35 @@ func CreateUserActiveCode(u *m.User, startInf interface{}) string { code += hex.EncodeToString([]byte(u.Login)) return code } + +// // 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/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 04dd8785f96..8470aa06b83 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -28,8 +28,8 @@ func TestNotifications(t *testing.T) { Convey("When sending reset email password", func() { sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}}) - So(sentMail.Body, ShouldContainSubstring, "h2") - So(sentMail.Subject, ShouldEqual, "Welcome to Grafana") + So(sentMail.Body, ShouldContainSubstring, "body") + So(sentMail.Subject, ShouldEqual, "Reset your Grafana password") So(sentMail.Body, ShouldNotContainSubstring, "Subject") }) }) diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index 767f788cea3..40e8009b399 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -21,13 +21,10 @@ function (angular, config) { $scope.disableUserSignUp = config.disableUserSignUp; $scope.loginMode = true; - $scope.submitBtnClass = 'btn-inverse'; $scope.submitBtnText = 'Log in'; - $scope.strengthClass = ''; $scope.init = function() { $scope.$watch("loginMode", $scope.loginModeChanged); - $scope.passwordChanged(); var params = $location.search(); if (params.failedMsg) { @@ -56,27 +53,6 @@ function (angular, config) { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; - $scope.passwordChanged = function(newValue) { - if (!newValue) { - $scope.strengthText = ""; - $scope.strengthClass = "hidden"; - return; - } - if (newValue.length < 4) { - $scope.strengthText = "strength: weak sauce."; - $scope.strengthClass = "password-strength-bad"; - return; - } - if (newValue.length <= 8) { - $scope.strengthText = "strength: you can do better."; - $scope.strengthClass = "password-strength-ok"; - return; - } - - $scope.strengthText = "strength: strong like a bull."; - $scope.strengthClass = "password-strength-good"; - }; - $scope.signUp = function() { if (!$scope.loginForm.$valid) { return; diff --git a/public/app/controllers/resetPasswordCtrl.js b/public/app/controllers/resetPasswordCtrl.js index 385a52320d1..5d183d891ae 100644 --- a/public/app/controllers/resetPasswordCtrl.js +++ b/public/app/controllers/resetPasswordCtrl.js @@ -1,25 +1,40 @@ define([ 'angular', - 'app', - 'lodash' ], -function (angular, app, _) { +function (angular) { 'use strict'; var module = angular.module('grafana.controllers'); - module.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv) { + module.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv, $location) { contextSrv.sidemenu = false; - $scope.sendMode = true; $scope.formModel = {}; + $scope.mode = 'send'; + + if ($location.search().code) { + $scope.mode = 'reset'; + } $scope.sendResetEmail = function() { - if (!$scope.loginForm.$valid) { + if (!$scope.sendResetForm.$valid) { + return; + } + backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + $scope.mode = 'email-sent'; + }); + }; + + $scope.submitReset = function() { + if (!$scope.resetForm.$valid) { return; } + + if ($scope.formModel.newPassword !== $scope.formModel.confirmPassword) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); return; } backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + $location.path('login'); }); }; diff --git a/public/app/directives/all.js b/public/app/directives/all.js index b92bc59ca41..21ca137b92b 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -18,4 +18,5 @@ define([ './topnav', './giveFocus', './annotationTooltip', + './passwordStrenght', ], function () {}); diff --git a/public/app/directives/passwordStrenght.js b/public/app/directives/passwordStrenght.js new file mode 100644 index 00000000000..f75a8fe8854 --- /dev/null +++ b/public/app/directives/passwordStrenght.js @@ -0,0 +1,47 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('passwordStrength', function() { + var template = '
' + + '{{strengthText}}' + + '
'; + return { + template: template, + scope: { + password: "=", + }, + link: function($scope) { + + $scope.strengthClass = ''; + + function passwordChanged(newValue) { + if (!newValue) { + $scope.strengthText = ""; + $scope.strengthClass = "hidden"; + return; + } + if (newValue.length < 4) { + $scope.strengthText = "strength: weak sauce."; + $scope.strengthClass = "password-strength-bad"; + return; + } + if (newValue.length <= 8) { + $scope.strengthText = "strength: you can do better."; + $scope.strengthClass = "password-strength-ok"; + return; + } + + $scope.strengthText = "strength: strong like a bull."; + $scope.strengthClass = "password-strength-good"; + } + + $scope.$watch("password", passwordChanged); + } + }; + }); +}); diff --git a/public/app/partials/login.html b/public/app/partials/login.html index ded98e6bd67..aa99daf85a6 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -42,7 +42,7 @@