Merge branch 'signup_remake'
This commit is contained in:
+1
-1
@@ -28,4 +28,4 @@ public/css/*.min.css
|
||||
conf/custom.ini
|
||||
fig.yml
|
||||
profile.cov
|
||||
|
||||
grafana
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
|
||||
+44
-32
@@ -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
|
||||
}
|
||||
|
||||
+108
-14
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
-7
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,6 +7,7 @@ define([
|
||||
'./jsonEditorCtrl',
|
||||
'./loginCtrl',
|
||||
'./invitedCtrl',
|
||||
'./signupCtrl',
|
||||
'./resetPasswordCtrl',
|
||||
'./sidemenuCtrl',
|
||||
'./errorCtrl',
|
||||
|
||||
@@ -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 + '/';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ define([
|
||||
'./panel/all',
|
||||
'./profile/profileCtrl',
|
||||
'./profile/changePasswordCtrl',
|
||||
'./profile/selectOrgCtrl',
|
||||
'./org/all',
|
||||
'./admin/all',
|
||||
], function () {});
|
||||
|
||||
@@ -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>
|
||||
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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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,6 +1,6 @@
|
||||
<div class="container">
|
||||
|
||||
<div class="login-page-background">
|
||||
<div class="signup-page-background">
|
||||
</div>
|
||||
|
||||
<div class="login-box">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user