diff --git a/.gitignore b/.gitignore index 882f4c09aeb..96824b93aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,4 @@ public/css/*.min.css conf/custom.ini fig.yml profile.cov - +grafana diff --git a/CHANGELOG.md b/CHANGELOG.md index db526bf7d62..bbe748bc565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ it allows you to add queries of differnet data source types & instances to the s - [Issue #2565](https://github.com/grafana/grafana/issues/2565). TimePicker: Fix for when you applied custom time range it did not refreh dashboard - [Issue #2563](https://github.com/grafana/grafana/issues/2563). Annotations: Fixed issue when html sanitizer failes for title to annotation body, now fallbacks to html escaping title and text - [Issue #2564](https://github.com/grafana/grafana/issues/2564). Templating: Another atempt at fixing #2534 (Init multi value template var used in repeat panel from url) +- [Issue #2620](https://github.com/grafana/grafana/issues/2620). Graph: multi series tooltip did no highlight correct point when stacking was enabled and series were of different resolution **Breaking Changes** - Notice to makers/users of custom data sources, there is a minor breaking change in 2.2 that diff --git a/conf/defaults.ini b/conf/defaults.ini index 8eafec90e0d..6ebf5fac01d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -134,6 +134,9 @@ auto_assign_org = true # Default role new users will be automatically assigned (if auto_assign_org above is set to true) auto_assign_org_role = Viewer +# Require email validation before sign up completes +verify_email_enabled = false + #################################### Anonymous Auth ########################## [auth.anonymous] # enable anonymous access diff --git a/emails/assets/css/style.css b/emails/assets/css/style.css index 7d5d7a7f759..f138a0c1296 100644 --- a/emails/assets/css/style.css +++ b/emails/assets/css/style.css @@ -109,8 +109,8 @@ table.columns td.better-button { } .better-button a { - text-decoration: none; - -webkit-border-radius: 2px; + text-decoration: none; + -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; @@ -123,7 +123,7 @@ table.columns td.better-button { .better-button:hover a { color: #FFFFFF !important; background-color: #F2821E; - border: 1px solid #F2821E; + border: 1px solid #F2821E; } .better-button:visited a { @@ -132,4 +132,13 @@ table.columns td.better-button { .better-button:active a { color: #FFFFFF !important; -} \ No newline at end of file +} + +.verification-code { + background-color: #EEEEEE; + padding: 3px; + margin: 8px; + display: inline-block; + font-weight: bold; + font-size: 20px; +} diff --git a/emails/templates/invited_to_org.html b/emails/templates/invited_to_org.html index 679c0062131..59e76cd5a77 100644 --- a/emails/templates/invited_to_org.html +++ b/emails/templates/invited_to_org.html @@ -32,17 +32,14 @@ - - - - -
Log in now
- - + + + + +
Log in now
- diff --git a/emails/templates/signup_started.html b/emails/templates/signup_started.html new file mode 100644 index 00000000000..39b369a1f7c --- /dev/null +++ b/emails/templates/signup_started.html @@ -0,0 +1,46 @@ +[[Subject .Subject "Welcome to Grafana, please complete your sign up!"]] + + + + + +
+ + + + + + +
+

Complete the signup

+
+ +
+ + + + + +
+ + + + + + + + +
+ Copy and past the email verification code:
+ [[.Code]]
in + the sign up form or use the link below. +
+ + + + +
Complete Sign Up
+
+
+ + diff --git a/pkg/api/api.go b/pkg/api/api.go index ef89311bdd6..1a876c3bc70 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -43,7 +43,9 @@ func Register(r *macaron.Macaron) { // sign up r.Get("/signup", Index) - r.Post("/api/user/signup", bind(m.CreateUserCommand{}), wrap(SignUp)) + r.Get("/api/user/signup/options", wrap(GetSignUpOptions)) + r.Post("/api/user/signup", bind(dtos.SignUpForm{}), wrap(SignUp)) + r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2)) // invited r.Get("/api/user/invite/:code", wrap(GetInviteInfoByCode)) diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 9b407535429..dbbe24a159b 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -1,5 +1,18 @@ package dtos +type SignUpForm struct { + Email string `json:"email" binding:"Required"` +} + +type SignUpStep2Form struct { + Email string `json:"email"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Code string `json:"code"` + OrgName string `json:"orgName"` +} + type AdminCreateUserForm struct { Email string `json:"email"` Login string `json:"login"` diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index e4e0de126e9..b7d8d213853 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -14,7 +14,7 @@ import ( ) func GetPendingOrgInvites(c *middleware.Context) Response { - query := m.GetTempUsersForOrgQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending} + query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get invites from db", err) @@ -111,13 +111,8 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto } func RevokeInvite(c *middleware.Context) Response { - cmd := m.UpdateTempUserStatusCommand{ - Code: c.Params(":code"), - Status: m.TmpUserRevoked, - } - - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to update invite status", err) + if ok, rsp := updateTempUserStatus(c.Params(":code"), m.TmpUserRevoked); !ok { + return rsp } return ApiSuccess("Invite revoked") @@ -169,38 +164,55 @@ func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteFor return ApiError(500, "failed to create user", err) } - user := cmd.Result + user := &cmd.Result - bus.Publish(&events.UserSignedUp{ - Id: user.Id, - Name: user.Name, + bus.Publish(&events.SignUpCompleted{ + Name: user.NameOrFallback(), Email: user.Email, - Login: user.Login, }) - // add to org - addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role} - if err := bus.Dispatch(&addOrgUserCmd); err != nil { - if err != m.ErrOrgUserAlreadyAdded { - return ApiError(500, "Error while trying to create org user", err) - } + if ok, rsp := applyUserInvite(user, invite, true); !ok { + return rsp } - // set org to active - if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil { - return ApiError(500, "Failed to set org as active", err) - } + loginUserWithUser(user, c) - // update temp user status - updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: invite.Code, Status: m.TmpUserCompleted} - if err := bus.Dispatch(&updateTmpUserCmd); err != nil { - return ApiError(500, "Failed to update invite status", err) - } - - loginUserWithUser(&user, c) - - metrics.M_Api_User_SignUp.Inc(1) + metrics.M_Api_User_SignUpCompleted.Inc(1) metrics.M_Api_User_SignUpInvite.Inc(1) return ApiSuccess("User created and logged in") } + +func updateTempUserStatus(code string, status m.TempUserStatus) (bool, Response) { + // update temp user status + updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: code, Status: status} + if err := bus.Dispatch(&updateTmpUserCmd); err != nil { + return false, ApiError(500, "Failed to update invite status", err) + } + + return true, nil +} + +func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool, Response) { + // add to org + addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role} + if err := bus.Dispatch(&addOrgUserCmd); err != nil { + if err != m.ErrOrgUserAlreadyAdded { + return false, ApiError(500, "Error while trying to create org user", err) + } + } + + // update temp user status + if ok, rsp := updateTempUserStatus(invite.Code, m.TmpUserCompleted); !ok { + return false, rsp + } + + if setActive { + // set org to active + if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil { + return false, ApiError(500, "Failed to set org as active", err) + } + } + + return true, nil +} diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 77305caba70..fa450b93df2 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -1,38 +1,132 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) +// GET /api/user/signup/options +func GetSignUpOptions(c *middleware.Context) Response { + return Json(200, util.DynMap{ + "verifyEmailEnabled": setting.VerifyEmailEnabled, + "autoAssignOrg": setting.AutoAssignOrg, + }) +} + // POST /api/user/signup -func SignUp(c *middleware.Context, cmd m.CreateUserCommand) Response { +func SignUp(c *middleware.Context, form dtos.SignUpForm) Response { if !setting.AllowUserSignUp { return ApiError(401, "User signup is disabled", nil) } - cmd.Login = cmd.Email - - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "failed to create user", err) + existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email} + if err := bus.Dispatch(&existing); err == nil { + return ApiError(422, "User with same email address already exists", nil) } - user := cmd.Result + cmd := m.CreateTempUserCommand{} + cmd.OrgId = -1 + cmd.Email = form.Email + cmd.Status = m.TmpUserSignUpStarted + cmd.InvitedByUserId = c.UserId + cmd.Code = util.GetRandomString(20) + cmd.RemoteAddr = c.Req.RemoteAddr - bus.Publish(&events.UserSignedUp{ - Id: user.Id, - Name: user.Name, - Email: user.Email, - Login: user.Login, + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to create signup", err) + } + + bus.Publish(&events.SignUpStarted{ + Email: form.Email, + Code: cmd.Code, }) - loginUserWithUser(&user, c) + metrics.M_Api_User_SignUpStarted.Inc(1) - metrics.M_Api_User_SignUp.Inc(1) + return Json(200, util.DynMap{"status": "SignUpCreated"}) +} - return ApiSuccess("User created and logged in") +func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response { + if !setting.AllowUserSignUp { + return ApiError(401, "User signup is disabled", nil) + } + + createUserCmd := m.CreateUserCommand{ + Email: form.Email, + Login: form.Username, + Name: form.Name, + Password: form.Password, + OrgName: form.OrgName, + } + + if setting.VerifyEmailEnabled { + if ok, rsp := verifyUserSignUpEmail(form.Email, form.Code); !ok { + return rsp + } + createUserCmd.EmailVerified = true + } + + existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email} + if err := bus.Dispatch(&existing); err == nil { + return ApiError(401, "User with same email address already exists", nil) + } + + if err := bus.Dispatch(&createUserCmd); err != nil { + return ApiError(500, "Failed to create user", err) + } + + // publish signup event + user := &createUserCmd.Result + bus.Publish(&events.SignUpCompleted{ + Email: user.Email, + Name: user.NameOrFallback(), + }) + + // mark temp user as completed + if ok, rsp := updateTempUserStatus(form.Code, m.TmpUserCompleted); !ok { + return rsp + } + + // check for pending invites + invitesQuery := m.GetTempUsersQuery{Email: form.Email, Status: m.TmpUserInvitePending} + if err := bus.Dispatch(&invitesQuery); err != nil { + return ApiError(500, "Failed to query database for invites", err) + } + + apiResponse := util.DynMap{"message": "User sign up completed succesfully", "code": "redirect-to-landing-page"} + for _, invite := range invitesQuery.Result { + if ok, rsp := applyUserInvite(user, invite, false); !ok { + return rsp + } + apiResponse["code"] = "redirect-to-select-org" + } + + loginUserWithUser(user, c) + metrics.M_Api_User_SignUpCompleted.Inc(1) + + return Json(200, apiResponse) +} + +func verifyUserSignUpEmail(email string, code string) (bool, Response) { + query := m.GetTempUserByCodeQuery{Code: code} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrTempUserNotFound { + return false, ApiError(404, "Invalid email verification code", nil) + } + return false, ApiError(500, "Failed to read temp user", err) + } + + tempUser := query.Result + if tempUser.Email != email { + return false, ApiError(404, "Email verification code does not match email", nil) + } + + return true, nil } diff --git a/pkg/events/events.go b/pkg/events/events.go index 5e82578b474..235de9c3130 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -70,11 +70,15 @@ type UserCreated struct { Email string `json:"email"` } -type UserSignedUp struct { +type SignUpStarted struct { + Timestamp time.Time `json:"timestamp"` + Email string `json:"email"` + Code string `json:"code"` +} + +type SignUpCompleted struct { Timestamp time.Time `json:"timestamp"` - Id int64 `json:"id"` Name string `json:"name"` - Login string `json:"login"` Email string `json:"email"` } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 0850cbebc06..8e10b2428b4 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -13,14 +13,15 @@ var ( M_Api_Status_500 = NewComboCounterRef("api.status.500") M_Api_Status_404 = NewComboCounterRef("api.status.404") - M_Api_User_SignUp = NewComboCounterRef("api.user.signup") - M_Api_User_SignUpInvite = NewComboCounterRef("api.user.signup_invite") - M_Api_Dashboard_Get = NewComboCounterRef("api.dashboard.get") - M_Api_Dashboard_Post = NewComboCounterRef("api.dashboard.post") - M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create") - M_Api_Login_Post = NewComboCounterRef("api.login.post") - M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") - M_Api_Org_Create = NewComboCounterRef("api.org.create") + M_Api_User_SignUpStarted = NewComboCounterRef("api.user.signup_started") + M_Api_User_SignUpCompleted = NewComboCounterRef("api.user.signup_completed") + M_Api_User_SignUpInvite = NewComboCounterRef("api.user.signup_invite") + M_Api_Dashboard_Get = NewComboCounterRef("api.dashboard.get") + M_Api_Dashboard_Post = NewComboCounterRef("api.dashboard.post") + M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create") + M_Api_Login_Post = NewComboCounterRef("api.login.post") + M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") + M_Api_Org_Create = NewComboCounterRef("api.org.create") M_Api_Dashboard_Snapshot_Create = NewComboCounterRef("api.dashboard_snapshot.create") M_Api_Dashboard_Snapshot_External = NewComboCounterRef("api.dashboard_snapshot.external") diff --git a/pkg/models/temp_user.go b/pkg/models/temp_user.go index 6aeb67cda9b..00c496c4844 100644 --- a/pkg/models/temp_user.go +++ b/pkg/models/temp_user.go @@ -13,9 +13,9 @@ var ( type TempUserStatus string const ( + TmpUserSignUpStarted TempUserStatus = "SignUpStarted" TmpUserInvitePending TempUserStatus = "InvitePending" TmpUserCompleted TempUserStatus = "Completed" - TmpUserEmailPending TempUserStatus = "EmailPending" TmpUserRevoked TempUserStatus = "Revoked" ) @@ -60,8 +60,9 @@ type UpdateTempUserStatusCommand struct { Status TempUserStatus } -type GetTempUsersForOrgQuery struct { +type GetTempUsersQuery struct { OrgId int64 + Email string Status TempUserStatus Result []*TempUserDTO diff --git a/pkg/models/user.go b/pkg/models/user.go index bf697676b32..30ab34db9a6 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -44,14 +44,16 @@ func (u *User) NameOrFallback() string { // COMMANDS type CreateUserCommand struct { - Email string `json:"email" binding:"Required"` - Login string `json:"login"` - Name string `json:"name"` - Company string `json:"compay"` - Password string `json:"password" binding:"Required"` - IsAdmin bool `json:"-"` + Email string + Login string + Name string + Company string + OrgName string + Password string + EmailVerified bool + IsAdmin bool - Result User `json:"-"` + Result User } type UpdateUserCommand struct { diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 401cd812d5e..576fe828331 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -3,7 +3,9 @@ package notifications import ( "bytes" "errors" + "fmt" "html/template" + "net/url" "path/filepath" "github.com/grafana/grafana/pkg/bus" @@ -16,6 +18,7 @@ import ( var mailTemplates *template.Template var tmplResetPassword = "reset_password.html" +var tmplSignUpStarted = "signup_started.html" var tmplWelcomeOnSignUp = "welcome_on_signup.html" func Init() error { @@ -25,7 +28,8 @@ func Init() error { bus.AddHandler("email", validateResetPasswordCode) bus.AddHandler("email", sendEmailCommandHandler) - bus.AddEventListener(userSignedUpHandler) + bus.AddEventListener(signUpStartedHandler) + bus.AddEventListener(signUpCompletedHandler) mailTemplates = template.New("name") mailTemplates.Funcs(template.FuncMap{ @@ -120,18 +124,38 @@ func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { return nil } -func userSignedUpHandler(evt *events.UserSignedUp) error { - log.Info("User signed up: %s, send_option: %s", evt.Email, setting.Smtp.SendWelcomeEmailOnSignUp) +func signUpStartedHandler(evt *events.SignUpStarted) error { + if !setting.VerifyEmailEnabled { + return nil + } + log.Info("User signup started: %s", evt.Email) + + if evt.Email == "" { + return nil + } + + return sendEmailCommandHandler(&m.SendEmailCommand{ + To: []string{evt.Email}, + Template: tmplSignUpStarted, + Data: map[string]interface{}{ + "Email": evt.Email, + "Code": evt.Code, + "SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))), + }, + }) +} + +func signUpCompletedHandler(evt *events.SignUpCompleted) error { if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp { return nil } return sendEmailCommandHandler(&m.SendEmailCommand{ To: []string{evt.Email}, - Template: tmplWelcomeOnSignUp, + Template: tmplSignUpStarted, Data: map[string]interface{}{ - "Name": evt.Login, + "Name": evt.Name, }, }) } diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go index 0cac59b21f6..0fe5c9612f5 100644 --- a/pkg/services/sqlstore/temp_user.go +++ b/pkg/services/sqlstore/temp_user.go @@ -10,7 +10,7 @@ import ( func init() { bus.AddHandler("sql", CreateTempUser) - bus.AddHandler("sql", GetTempUsersForOrg) + bus.AddHandler("sql", GetTempUsersQuery) bus.AddHandler("sql", UpdateTempUserStatus) bus.AddHandler("sql", GetTempUserByCode) } @@ -49,8 +49,8 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error { }) } -func GetTempUsersForOrg(query *m.GetTempUsersForOrgQuery) error { - var rawSql = `SELECT +func GetTempUsersQuery(query *m.GetTempUsersQuery) error { + rawSql := `SELECT tu.id as id, tu.org_id as org_id, tu.email as email, @@ -66,10 +66,23 @@ func GetTempUsersForOrg(query *m.GetTempUsersForOrgQuery) error { u.email as invited_by_email FROM ` + dialect.Quote("temp_user") + ` as tu LEFT OUTER JOIN ` + dialect.Quote("user") + ` as u on u.id = tu.invited_by_user_id - WHERE tu.org_id=? AND tu.status =? ORDER BY tu.created desc` + WHERE tu.status=?` + params := []interface{}{string(query.Status)} + + if query.OrgId > 0 { + rawSql += ` AND tu.org_id=?` + params = append(params, query.OrgId) + } + + if query.Email != "" { + rawSql += ` AND tu.email=?` + params = append(params, query.Email) + } + + rawSql += " ORDER BY tu.created desc" query.Result = make([]*m.TempUserDTO, 0) - sess := x.Sql(rawSql, query.OrgId, string(query.Status)) + sess := x.Sql(rawSql, params...) err := sess.Find(&query.Result) return err } diff --git a/pkg/services/sqlstore/temp_user_test.go b/pkg/services/sqlstore/temp_user_test.go index 6b550799e73..ebf753890f6 100644 --- a/pkg/services/sqlstore/temp_user_test.go +++ b/pkg/services/sqlstore/temp_user_test.go @@ -25,8 +25,16 @@ func TestTempUserCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) Convey("Should be able to get temp users by org id", func() { - query := m.GetTempUsersForOrgQuery{OrgId: 2256, Status: m.TmpUserInvitePending} - err = GetTempUsersForOrg(&query) + query := m.GetTempUsersQuery{OrgId: 2256, Status: m.TmpUserInvitePending} + err = GetTempUsersQuery(&query) + + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + }) + + Convey("Should be able to get temp users by email", func() { + query := m.GetTempUsersQuery{Email: "e@as.co", Status: m.TmpUserInvitePending} + err = GetTempUsersQuery(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index cbca8f1253a..b26000673d6 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -45,7 +45,10 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) org.Id = 1 } } else { - org.Name = util.StringsFallback2(cmd.Email, cmd.Login) + org.Name = cmd.OrgName + if len(org.Name) == 0 { + org.Name = util.StringsFallback2(cmd.Email, cmd.Login) + } } org.Created = time.Now() @@ -77,14 +80,15 @@ func CreateUser(cmd *m.CreateUserCommand) error { // create user user := m.User{ - Email: cmd.Email, - Name: cmd.Name, - Login: cmd.Login, - Company: cmd.Company, - IsAdmin: cmd.IsAdmin, - OrgId: orgId, - Created: time.Now(), - Updated: time.Now(), + Email: cmd.Email, + Name: cmd.Name, + Login: cmd.Login, + Company: cmd.Company, + IsAdmin: cmd.IsAdmin, + OrgId: orgId, + EmailVerified: cmd.EmailVerified, + Created: time.Now(), + Updated: time.Now(), } if len(cmd.Password) > 0 { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 43bc8f6247d..a29d54eb662 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -79,6 +79,7 @@ var ( AllowUserOrgCreate bool AutoAssignOrg bool AutoAssignOrgRole string + VerifyEmailEnabled bool // Http auth AdminUser string @@ -393,6 +394,7 @@ func NewConfigContext(args *CommandLineArgs) { AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"}) + VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) @@ -424,6 +426,10 @@ func NewConfigContext(args *CommandLineArgs) { readSessionConfig() readSmtpSettings() + + if VerifyEmailEnabled && !Smtp.Enabled { + log.Warn("require_email_validation is enabled but smpt is disabled") + } } func readSessionConfig() { diff --git a/public/app/controllers/all.js b/public/app/controllers/all.js index 500feea3516..3fdaf6ef3e9 100644 --- a/public/app/controllers/all.js +++ b/public/app/controllers/all.js @@ -7,6 +7,7 @@ define([ './jsonEditorCtrl', './loginCtrl', './invitedCtrl', + './signupCtrl', './resetPasswordCtrl', './sidemenuCtrl', './errorCtrl', diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index 40e8009b399..0ee370fb979 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -58,8 +58,12 @@ function (angular, config) { return; } - backendSrv.post('/api/user/signup', $scope.formModel).then(function() { - window.location.href = config.appSubUrl + '/'; + backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) { + if (result.status === 'SignUpCreated') { + $location.path('/signup').search({email: $scope.formModel.email}); + } else { + window.location.href = config.appSubUrl + '/'; + } }); }; diff --git a/public/app/controllers/signupCtrl.js b/public/app/controllers/signupCtrl.js new file mode 100644 index 00000000000..c23e7b151c6 --- /dev/null +++ b/public/app/controllers/signupCtrl.js @@ -0,0 +1,49 @@ +define([ + 'angular', + 'config', +], +function (angular, config) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('SignUpCtrl', function($scope, $location, contextSrv, backendSrv) { + + contextSrv.sidemenu = false; + + $scope.formModel = {}; + + $scope.init = function() { + var params = $location.search(); + $scope.formModel.orgName = params.email; + $scope.formModel.email = params.email; + $scope.formModel.username = params.email; + $scope.formModel.code = params.code; + + $scope.verifyEmailEnabled = false; + $scope.autoAssignOrg = false; + + backendSrv.get('/api/user/signup/options').then(function(options) { + $scope.verifyEmailEnabled = options.verifyEmailEnabled; + $scope.autoAssignOrg = options.autoAssignOrg; + }); + }; + + $scope.submit = function() { + if (!$scope.signUpForm.$valid) { + return; + } + + backendSrv.post('/api/user/signup/step2', $scope.formModel).then(function(rsp) { + if (rsp.code === 'redirect-to-select-org') { + window.location.href = config.appSubUrl + '/profile/select-org?signup=1'; + } else { + window.location.href = config.appSubUrl + '/'; + } + }); + }; + + $scope.init(); + + }); +}); diff --git a/public/app/features/all.js b/public/app/features/all.js index 92db77a9a5d..1a000a71782 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -7,6 +7,7 @@ define([ './panel/all', './profile/profileCtrl', './profile/changePasswordCtrl', + './profile/selectOrgCtrl', './org/all', './admin/all', ], function () {}); diff --git a/public/app/features/profile/partials/select_org.html b/public/app/features/profile/partials/select_org.html new file mode 100644 index 00000000000..2e7a992b4fa --- /dev/null +++ b/public/app/features/profile/partials/select_org.html @@ -0,0 +1,53 @@ +
+ +
+
+ +
+ + + +
+

+   + Change active organization +

+ + + +
+ + + + +
+ + {{org.name}} ({{org.role}}) + +
+
+ +
+ + +
+ +
+ +
+ +
+ + diff --git a/public/app/features/profile/selectOrgCtrl.js b/public/app/features/profile/selectOrgCtrl.js new file mode 100644 index 00000000000..9a588151432 --- /dev/null +++ b/public/app/features/profile/selectOrgCtrl.js @@ -0,0 +1,33 @@ +define([ + 'angular', + 'config', +], +function (angular, config) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('SelectOrgCtrl', function($scope, backendSrv, contextSrv) { + + contextSrv.sidemenu = false; + + $scope.init = function() { + $scope.getUserOrgs(); + }; + + $scope.getUserOrgs = function() { + backendSrv.get('/api/user/orgs').then(function(orgs) { + $scope.orgs = orgs; + }); + }; + + $scope.setUsingOrg = function(org) { + backendSrv.post('/api/user/using/' + org.orgId).then(function() { + window.location.href = config.appSubUrl + '/'; + }); + }; + + $scope.init(); + + }); +}); diff --git a/public/app/partials/login.html b/public/app/partials/login.html index f311c929b66..2d9ed8d3b47 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -1,6 +1,4 @@
-
-
@@ -18,7 +16,7 @@
-
+
    @@ -43,7 +41,7 @@
-
+
  • Email @@ -55,21 +53,6 @@
-
-
    -
  • - Password -
  • -
  • - -
  • -
-
-
-
- -
-