Commit 6fac2414 by Torkel Ödegaard

Merge branch 'signup_remake'

parents 688ed405 99bb9d4f
......@@ -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;
}
\ No newline at end of file
}
.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>
......
[[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>
......@@ -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))
......
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"`
......
......@@ -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
}
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
}
......@@ -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
......
......@@ -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 {
......
......@@ -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 + '/';
}
});
};
......
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 () {});
<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>
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();
});
});
<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">
......
<div class="container">
<div class="login-page-background">
<div class="signup-page-background">
</div>
<div class="login-box">
......
<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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment