Torkel Ödegaard 10 лет назад
Родитель
Сommit
6fac241404

+ 1 - 1
.gitignore

@@ -28,4 +28,4 @@ public/css/*.min.css
 conf/custom.ini
 fig.yml
 profile.cov
-
+grafana

+ 1 - 0
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

+ 3 - 0
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

+ 13 - 4
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;
-}
+}
+
+.verification-code {
+  background-color: #EEEEEE;
+  padding: 3px;
+  margin: 8px;
+  display: inline-block;
+  font-weight: bold;
+  font-size: 20px;
+}

+ 5 - 8
emails/templates/invited_to_org.html

@@ -32,17 +32,14 @@
 				</tr>
 				<tr>
 					<td class="center">
-	                    <table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0">
-	                    	<tr>
-	                          <td align="center" class="better-button" bgcolor="#ff8f2b"><a href="[[.AppUrl]]" target="_blank">Log in now</a></td>
-	                        </tr>
-	                    </table>
-
-
+						<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0">
+							<tr>
+								<td align="center" class="better-button" bgcolor="#ff8f2b"><a href="[[.AppUrl]]" target="_blank">Log in now</a></td>
+							</tr>
+						</table>
 					</td>
 				</tr>
 			</table>
-
 		</td>
 	</tr>
 </table>

+ 46 - 0
emails/templates/signup_started.html

@@ -0,0 +1,46 @@
+[[Subject .Subject "Welcome to Grafana, please complete your sign up!"]]
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+
+			<table class="twelve columns">
+				<tr>
+					<td>
+						<h3 class="center">Complete the signup</h3>
+					</td>
+					<td class="expander"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row">
+	<tr>
+		<td class="wrapper last">
+			<table class="twelve columns">
+				<tr>
+					<td class="center">
+						Copy and past the email verification code:<br>
+						<span class="verification-code">[[.Code]]</span><br> in
+						the sign up form <strong>or</strong> use the link below.
+					</td>
+					<td class="expander"></td>
+				</tr>
+				<tr>
+					<td class="center">
+						<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0">
+							<tr>
+								<td align="center" class="better-button" bgcolor="#ff8f2b"><a href="[[.SignUpUrl]]" target="_blank">Complete Sign Up</a></td>
+							</tr>
+						</table>
+					</td>
+				</tr>
+			</table>
+		</td>
+	</tr>
+</table>
+
+

+ 3 - 1
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))

+ 13 - 0
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"`

+ 39 - 27
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,
 	})
 
+	if ok, rsp := applyUserInvite(user, invite, true); !ok {
+		return rsp
+	}
+
+	loginUserWithUser(user, c)
+
+	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 ApiError(500, "Error while trying to create org user", err)
+			return false, ApiError(500, "Error while trying to create org user", err)
 		}
 	}
 
-	// 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)
-	}
-
 	// 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)
+	if ok, rsp := updateTempUserStatus(invite.Code, m.TmpUserCompleted); !ok {
+		return false, rsp
 	}
 
-	loginUserWithUser(&user, c)
-
-	metrics.M_Api_User_SignUp.Inc(1)
-	metrics.M_Api_User_SignUpInvite.Inc(1)
+	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 ApiSuccess("User created and logged in")
+	return true, nil
 }

+ 105 - 11
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
+	existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
+	if err := bus.Dispatch(&existing); err == nil {
+		return ApiError(422, "User with same email address already exists", nil)
+	}
+
+	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
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "failed to create user", err)
+		return ApiError(500, "Failed to create signup", err)
 	}
 
-	user := cmd.Result
+	bus.Publish(&events.SignUpStarted{
+		Email: form.Email,
+		Code:  cmd.Code,
+	})
+
+	metrics.M_Api_User_SignUpStarted.Inc(1)
+
+	return Json(200, util.DynMap{"status": "SignUpCreated"})
+}
+
+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
+	}
 
-	bus.Publish(&events.UserSignedUp{
-		Id:    user.Id,
-		Name:  user.Name,
+	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,
-		Login: user.Login,
+		Name:  user.NameOrFallback(),
 	})
 
-	loginUserWithUser(&user, c)
+	// 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)
+}
 
-	metrics.M_Api_User_SignUp.Inc(1)
+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 ApiSuccess("User created and logged in")
+	return true, nil
 }

+ 7 - 3
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"`
 }
 

+ 9 - 8
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")

+ 3 - 2
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

+ 10 - 8
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:"-"`
-
-	Result User `json:"-"`
+	Email         string
+	Login         string
+	Name          string
+	Company       string
+	OrgName       string
+	Password      string
+	EmailVerified bool
+	IsAdmin       bool
+
+	Result User
 }
 
 type UpdateUserCommand struct {

+ 29 - 5
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,
 		},
 	})
 }

+ 18 - 5
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
 }

+ 10 - 2
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)

+ 13 - 9
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 {

+ 6 - 0
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() {

+ 1 - 0
public/app/controllers/all.js

@@ -7,6 +7,7 @@ define([
   './jsonEditorCtrl',
   './loginCtrl',
   './invitedCtrl',
+  './signupCtrl',
   './resetPasswordCtrl',
   './sidemenuCtrl',
   './errorCtrl',

+ 6 - 2
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 + '/';
+        }
       });
     };
 

+ 49 - 0
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();
+
+  });
+});

+ 1 - 0
public/app/features/all.js

@@ -7,6 +7,7 @@ define([
   './panel/all',
   './profile/profileCtrl',
   './profile/changePasswordCtrl',
+  './profile/selectOrgCtrl',
   './org/all',
   './admin/all',
 ], function () {});

+ 53 - 0
public/app/features/profile/partials/select_org.html

@@ -0,0 +1,53 @@
+<div class="container">
+
+	<div class="signup-page-background">
+	</div>
+
+	<div class="login-box">
+
+		<div class="login-box-logo">
+			<img src="img/logo_transparent_200x75.png">
+		</div>
+
+    <div class="invite-box">
+			<h3>
+				<i class="fa fa-users"></i>&nbsp;
+				Change active organization
+			</h3>
+
+			<div class="modal-tagline">
+				You have been added to another Organization <br>
+				due to an open invitation!
+				<br><br>
+
+				Please select which organization you want to <br>
+				use right now (you can change this later at any time).
+			</div>
+
+			<div style="display: inline-block; width: 400px; margin: 30px 0">
+				<table class="grafana-options-table">
+					<tr ng-repeat="org in orgs">
+						<td class="nobg max-width-btns">
+							<a ng-click="setUsingOrg(org)" class="btn btn-inverse">
+								{{org.name}} ({{org.role}})
+							</a>
+						</td>
+					</tr>
+				</table>
+			</div>
+
+		</div>
+
+
+		<div class="row" style="margin-top: 50px">
+			<div class="version-footer text-center small">
+				Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
+				build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
+			</div>
+		</div>
+
+	</div>
+
+</div>
+
+

+ 33 - 0
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();
+
+  });
+});

+ 2 - 19
public/app/partials/login.html

@@ -1,6 +1,4 @@
 <div class="container">
-	<div class="login-page-background">
-	</div>
 
 	<div class="login-box">
 
@@ -18,7 +16,7 @@
 				</button>
 			</div>
 
-      <form name="loginForm" class="login-form">
+      <form name="loginForm" class="login-form" style="margin-top: 25px;">
 				<div class="tight-from-container">
 					<div class="tight-form" ng-if="loginMode">
 						<ul class="tight-form-list">
@@ -43,7 +41,7 @@
 						<div class="clearfix"></div>
 					</div>
 
-					<div class="tight-form" ng-if="!loginMode">
+					<div class="tight-form" ng-if="!loginMode" style="margin: 20px 0 57px 0">
 						<ul class="tight-form-list">
 							<li class="tight-form-item" style="width: 79px">
 								<strong>Email</strong>
@@ -55,21 +53,6 @@
 						<div class="clearfix"></div>
 					</div>
 
-					<div class="tight-form" ng-if="!loginMode">
-						<ul class="tight-form-list">
-							<li class="tight-form-item" style="width: 79px">
-								<strong>Password</strong>
-							</li>
-							<li>
-								<input type="password" class="tight-form-input last" watch-change="formModel.password = inputValue;" ng-minlength="4" required ng-model='formModel.password' placeholder="password" style="width: 253px">
-							</li>
-						</ul>
-						<div class="clearfix"></div>
-					</div>
-				</div>
-
-				<div ng-if="!loginMode" style="margin-left: 97px; width: 254px;">
-					<password-strength password="formModel.password"></password-strength>
 				</div>
 
 				<div class="login-submit-button-row">

+ 1 - 1
public/app/partials/signup_invited.html

@@ -1,6 +1,6 @@
 <div class="container">
 
-	<div class="login-page-background">
+	<div class="signup-page-background">
 	</div>
 
 	<div class="login-box">

+ 113 - 0
public/app/partials/signup_step2.html

@@ -0,0 +1,113 @@
+<div class="container">
+
+	<div class="signup-page-background">
+	</div>
+
+	<div class="login-box">
+
+		<div class="login-box-logo">
+			<img src="img/logo_transparent_200x75.png">
+		</div>
+
+    <div class="invite-box">
+			<h3>
+				You're almost there.
+			</h3>
+
+			<div class="modal-tagline">
+				We just need a couple of more bits of<br> information to finish creating your account.
+			</div>
+
+			<div style="display: inline-block; margin-top: 25px; width: 300px">
+					<div class="editor-option">
+						<label class="small">Your email:</label>
+						<span class="large">{{formModel.email}}</span>
+					</div>
+			</div>
+
+			<br>
+
+			<form name="signUpForm" class="login-form">
+
+				<div style="display: inline-block; margin-bottom: 25px; width: 300px" ng-if="verifyEmailEnabled">
+					<div class="editor-option">
+						<label class="small">Email verification code: (sent to your email)</label>
+						<input type="text" class="input input-xlarge text-center" ng-model="formModel.code" required></input>
+					</div>
+				</div>
+
+				<div class="tight-from-container">
+					<div class="tight-form" ng-if="!autoAssignOrg">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Organization name
+							</li>
+							<li>
+								<input type="text" name="orgName" class="tight-form-input last" ng-model='formModel.orgName' placeholder="Name your organization" style="width: 253px">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Your name
+							</li>
+							<li>
+								<input type="text" name="name" class="tight-form-input last" ng-model='formModel.name' placeholder="(optional)" style="width: 253px">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Username
+							</li>
+							<li>
+								<input type="text" class="tight-form-input last" required ng-model='formModel.username' placeholder="Username" style="width: 253px" autocomplete="off">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+
+					<div class="tight-form">
+						<ul class="tight-form-list">
+							<li class="tight-form-item" style="width: 128px">
+								Password
+							</li>
+							<li>
+								<input type="password" class="tight-form-input last" required ng-model="formModel.password" id="inputPassword" style="width: 253px" placeholder="password" autocomplete="off">
+							</li>
+						</ul>
+						<div class="clearfix"></div>
+					</div>
+				</div>
+
+				<div style="margin-left: 147px; width: 254px;">
+					<password-strength password="formModel.password"></password-strength>
+				</div>
+
+				<div class="login-submit-button-row">
+					<button type="submit" class="btn" ng-click="submit();" ng-class="{'btn-inverse': !signUpForm.$valid, 'btn-primary': signUpForm.$valid}">
+						Continue
+					</button>
+				</div>
+			</form>
+
+			<div class="clearfix"></div>
+		</div>
+
+		<div class="row" style="margin-top: 50px">
+			<div class="version-footer text-center small">
+				Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
+				build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
+			</div>
+		</div>
+
+	</div>
+
+</div>
+
+

+ 8 - 0
public/app/routes/all.js

@@ -74,6 +74,10 @@ define([
         templateUrl: 'app/features/profile/partials/password.html',
         controller : 'ChangePasswordCtrl',
       })
+      .when('/profile/select-org', {
+        templateUrl: 'app/features/profile/partials/select_org.html',
+        controller : 'SelectOrgCtrl',
+      })
       .when('/admin/settings', {
         templateUrl: 'app/features/admin/partials/settings.html',
         controller : 'AdminSettingsCtrl',
@@ -106,6 +110,10 @@ define([
         templateUrl: 'app/partials/signup_invited.html',
         controller : 'InvitedCtrl',
       })
+      .when('/signup', {
+        templateUrl: 'app/partials/signup_step2.html',
+        controller : 'SignUpCtrl',
+      })
       .when('/user/password/send-reset-email', {
         templateUrl: 'app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',

+ 5 - 6
public/app/services/backendSrv.js

@@ -37,17 +37,16 @@ function (angular, _, config) {
           return;
         }
 
-        if (err.status === 422) {
-          alertSrv.set("Validation failed", "", "warning", 4000);
-          throw err.data;
-        }
-
         var data = err.data || { message: 'Unexpected error' };
-
         if (_.isString(data)) {
           data = { message: data };
         }
 
+        if (err.status === 422) {
+          alertSrv.set("Validation failed", data.message, "warning", 4000);
+          throw data;
+        }
+
         data.severity = 'error';
 
         if (err.status < 500) {

+ 4 - 4
public/css/less/login.less

@@ -4,7 +4,7 @@
   float: left;
   margin-left: 25%;
   margin-right: 25%;
-  padding-top: 50px;
+  padding-top: 25px;
 }
 
 .login-box {
@@ -93,7 +93,7 @@
   }
 }
 
-.login-page-background {
+.signup-page-background {
   position: fixed;
   top: 0;
   left: 0;
@@ -101,8 +101,8 @@
   bottom: 0;
   height: 100%;
   width: 100%;
-  background-image: url(/img/background_tease.jpg);
-  opacity: 0.05;
+  background-image: url(../img/background_tease.jpg);
+  opacity: 0.3;
   z-index: -1;
 }
 

+ 5 - 8
public/emails/invited_to_org.html

@@ -147,17 +147,14 @@ color: #FFFFFF !important;
 				</tr>
 				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
 					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
-	                    <table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
-	                    	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
-	                          <td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
-	                        </tr>
-	                    </table>
-
-
+						<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
+							<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+								<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
+							</tr>
+						</table>
 					</td>
 				</tr>
 			</table>
-
 		</td>
 	</tr>
 </table>

+ 194 - 0
public/emails/signup_started.html

@@ -0,0 +1,194 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+	<meta name="viewport" content="width=device-width" />
+   
+</head>
+<body style="-ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0; min-width: 100%; padding: 0; text-align: left; width: 100% !important"><style type="text/css">
+body {
+width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0;
+}
+img {
+outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block;
+}
+body {
+color: #222222; font-family: "Helvetica", "Arial", sans-serif; font-weight: normal; padding: 0; margin: 0; text-align: left; line-height: 1.3;
+}
+body {
+font-size: 14px; line-height: 19px;
+}
+a:hover {
+color: #2795b6 !important;
+}
+a:active {
+color: #2795b6 !important;
+}
+a:visited {
+color: #2ba6cb !important;
+}
+body {
+font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;
+}
+a:hover {
+color: #ff8f2b !important;
+}
+a:active {
+color: #F2821E !important;
+}
+a:visited {
+color: #E67612 !important;
+}
+.better-button:hover a {
+color: #FFFFFF !important; background-color: #F2821E; border: 1px solid #F2821E;
+}
+.better-button:visited a {
+color: #FFFFFF !important;
+}
+.better-button:active a {
+color: #FFFFFF !important;
+}
+@media only screen and (max-width: 600px) {
+  table[class="body"] img {
+    width: auto !important; height: auto !important;
+  }
+  table[class="body"] center {
+    min-width: 0 !important;
+  }
+  table[class="body"] .container {
+    width: 95% !important;
+  }
+  table[class="body"] .row {
+    width: 100% !important; display: block !important;
+  }
+  table[class="body"] .wrapper {
+    display: block !important; padding-right: 0 !important;
+  }
+  table[class="body"] .columns {
+    table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important;
+  }
+  table[class="body"] table.columns td {
+    width: 100% !important;
+  }
+  table[class="body"] .columns td.six {
+    width: 50% !important;
+  }
+  table[class="body"] table.columns td.expander {
+    width: 1px !important;
+  }
+}
+</style>
+	<table class="body" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border-collapse: collapse; border-spacing: 0; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; height: 100%; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; width: 100%">
+		<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+			<td class="center" align="center" valign="top" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word">
+        <center style="min-width: 580px; width: 100%">
+
+          <table class="row header" style="background: #333; border-collapse: collapse; border-spacing: 0; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%" bgcolor="#333">
+            <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+              <td class="center" align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: center; vertical-align: top; word-break: break-word" valign="top">
+                <center style="min-width: 580px; width: 100%">
+
+                  <table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+                    <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                      <td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+                        <table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+                          <tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+                            <td class="six sub-columns center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; min-width: 0px; padding: 0px 10px 10px 0px; text-align: center; vertical-align: top; width: 50%; word-break: break-word" align="center" valign="top">
+															<img src="http://docs.grafana.org/img/logo_transparent_200x75.png" style="-ms-interpolation-mode: bicubic; clear: both; display: inline; float: none; max-width: 100%; outline: none; text-decoration: none; width: 150px" align="none" />
+                            </td>
+														<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+                          </tr>
+                        </table>
+
+                      </td>
+                    </tr>
+                  </table>
+
+                </center>
+              </td>
+            </tr>
+          </table>
+
+					<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
+						<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+							<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+								{{Subject .Subject "Welcome to Grafana, please complete your sign up!"}}
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+						<h3 class="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 22px; font-weight: normal; line-height: 1.3; margin: 20px 0 0; padding: 0; text-align: center; word-break: normal" align="center">Complete the signup</h3>
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+			</table>
+
+		</td>
+	</tr>
+</table>
+
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+	<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+		<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+			<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						Copy and past the email verification code:<br />
+						<span class="verification-code" style="background: #EEEEEE; display: inline-block; font-size: 20px; font-weight: bold; margin: 8px; padding: 3px">{{.Code}}</span><br /> in
+						the sign up form <strong>or</strong> use the link below.
+					</td>
+					<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+				</tr>
+				<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+					<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
+						<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
+							<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+								<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.SignUpUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Complete Sign Up</a></td>
+							</tr>
+						</table>
+					</td>
+				</tr>
+			</table>
+		</td>
+	</tr>
+</table>
+
+
+
+								
+								<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
+									<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+										<td class="wrapper last" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 10px 0px 0px; position: relative; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
+											<table class="twelve columns" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: left; vertical-align: top; width: 580px">
+												<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
+													<td align="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: left; vertical-align: top; word-break: break-word" valign="top">
+														<center style="min-width: 580px; width: 100%">
+															<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; line-height: 19px; margin: 0 0 10px; padding: 0; text-align: center" align="center">
+																Sent by <a href="{{.AppUrl}}" style="color: #E67612; text-decoration: none">Grafana v{{.BuildVersion}}</a>
+															</p>
+														</center>
+													</td>
+													<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top"></td>
+												</tr>
+											</table>
+										</td>
+									</tr>
+								</table>
+
+								
+							</td>
+						</tr>
+
+					</table>
+				</center>
+			</td>
+		</tr>
+
+	</table>
+</body>
+</html>