satellite/{web, console}: login captcha implemented
Implemented Recaptcha and Hcaptcha for login screen. Slightly refactored registration page implementation. Made 2 different login/registration captcha configs on server side to easily swap between captchas independently. Issue: https://github.com/storj/storj/issues/4982 Change-Id: I362bd5db2d59010e90a22301893bc3e1d860293a
This commit is contained in:
parent
799b159bba
commit
ad37ea4518
@ -62,6 +62,8 @@ storj.io/storj/satellite/console."login_mfa_passcode_success" Counter
|
|||||||
storj.io/storj/satellite/console."login_mfa_recovery_failure" Counter
|
storj.io/storj/satellite/console."login_mfa_recovery_failure" Counter
|
||||||
storj.io/storj/satellite/console."login_mfa_recovery_success" Counter
|
storj.io/storj/satellite/console."login_mfa_recovery_success" Counter
|
||||||
storj.io/storj/satellite/console."login_success" Counter
|
storj.io/storj/satellite/console."login_success" Counter
|
||||||
|
storj.io/storj/satellite/console."login_user_captcha_error" Counter
|
||||||
|
storj.io/storj/satellite/console."login_user_captcha_unsuccessful" Counter
|
||||||
storj.io/storj/satellite/console."login_user_failed_count" IntVal
|
storj.io/storj/satellite/console."login_user_failed_count" IntVal
|
||||||
storj.io/storj/satellite/contact."failed_dial" Event
|
storj.io/storj/satellite/contact."failed_dial" Event
|
||||||
storj.io/storj/satellite/contact."failed_ping_node" Event
|
storj.io/storj/satellite/contact."failed_ping_node" Event
|
||||||
|
@ -24,15 +24,15 @@ func (r mockRecaptcha) Verify(ctx context.Context, responseToken string, userIP
|
|||||||
return responseToken == validResponseToken, nil
|
return responseToken == validResponseToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRecaptcha ensures the reCAPTCHA service is working properly.
|
// TestRegistrationRecaptcha ensures that registration reCAPTCHA service is working properly.
|
||||||
func TestRecaptcha(t *testing.T) {
|
func TestRegistrationRecaptcha(t *testing.T) {
|
||||||
testplanet.Run(t, testplanet.Config{
|
testplanet.Run(t, testplanet.Config{
|
||||||
SatelliteCount: 1,
|
SatelliteCount: 1,
|
||||||
Reconfigure: testplanet.Reconfigure{
|
Reconfigure: testplanet.Reconfigure{
|
||||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
config.Console.Recaptcha.Enabled = true
|
config.Console.Captcha.Registration.Recaptcha.Enabled = true
|
||||||
config.Console.Recaptcha.SecretKey = "mySecretKey"
|
config.Console.Captcha.Registration.Recaptcha.SecretKey = "mySecretKey"
|
||||||
config.Console.Recaptcha.SiteKey = "mySiteKey"
|
config.Console.Captcha.Registration.Recaptcha.SiteKey = "mySiteKey"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
@ -67,3 +67,61 @@ func TestRecaptcha(t *testing.T) {
|
|||||||
require.True(t, console.ErrCaptcha.Has(err))
|
require.True(t, console.ErrCaptcha.Has(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLoginRecaptcha ensures that login reCAPTCHA service is working properly.
|
||||||
|
func TestLoginRecaptcha(t *testing.T) {
|
||||||
|
testplanet.Run(t, testplanet.Config{
|
||||||
|
SatelliteCount: 1,
|
||||||
|
Reconfigure: testplanet.Reconfigure{
|
||||||
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
|
config.Console.Captcha.Login.Recaptcha.Enabled = true
|
||||||
|
config.Console.Captcha.Login.Recaptcha.SecretKey = "mySecretKey"
|
||||||
|
config.Console.Captcha.Login.Recaptcha.SiteKey = "mySiteKey"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
|
service := planet.Satellites[0].API.Console.Service
|
||||||
|
require.NotNil(t, service)
|
||||||
|
service.TestSwapCaptchaHandler(mockRecaptcha{})
|
||||||
|
|
||||||
|
regToken, err := service.CreateRegToken(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
email := "user@mail.test"
|
||||||
|
password := "password"
|
||||||
|
|
||||||
|
user, err := service.CreateUser(ctx, console.CreateUser{
|
||||||
|
FullName: "User",
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
}, regToken.Secret)
|
||||||
|
|
||||||
|
require.NotNil(t, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
activationToken, err := service.GenerateActivationToken(ctx, user.ID, user.Email)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user, err = service.ActivateAccount(ctx, activationToken)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
token, err := service.Token(ctx, console.AuthUser{
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
CaptchaResponse: validResponseToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotEmpty(t, token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
token, err = service.Token(ctx, console.AuthUser{
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
CaptchaResponse: "wrong",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Empty(t, token)
|
||||||
|
require.True(t, console.ErrCaptcha.Has(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -435,10 +435,14 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
StorageTBPrice string
|
StorageTBPrice string
|
||||||
EgressTBPrice string
|
EgressTBPrice string
|
||||||
SegmentPrice string
|
SegmentPrice string
|
||||||
RecaptchaEnabled bool
|
RegistrationRecaptchaEnabled bool
|
||||||
RecaptchaSiteKey string
|
RegistrationRecaptchaSiteKey string
|
||||||
HcaptchaEnabled bool
|
RegistrationHcaptchaEnabled bool
|
||||||
HcaptchaSiteKey string
|
RegistrationHcaptchaSiteKey string
|
||||||
|
LoginRecaptchaEnabled bool
|
||||||
|
LoginRecaptchaSiteKey string
|
||||||
|
LoginHcaptchaEnabled bool
|
||||||
|
LoginHcaptchaSiteKey string
|
||||||
NewProjectDashboard bool
|
NewProjectDashboard bool
|
||||||
DefaultPaidStorageLimit memory.Size
|
DefaultPaidStorageLimit memory.Size
|
||||||
DefaultPaidBandwidthLimit memory.Size
|
DefaultPaidBandwidthLimit memory.Size
|
||||||
@ -474,10 +478,14 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
data.StorageTBPrice = server.pricing.StorageTBPrice
|
data.StorageTBPrice = server.pricing.StorageTBPrice
|
||||||
data.EgressTBPrice = server.pricing.EgressTBPrice
|
data.EgressTBPrice = server.pricing.EgressTBPrice
|
||||||
data.SegmentPrice = server.pricing.SegmentPrice
|
data.SegmentPrice = server.pricing.SegmentPrice
|
||||||
data.RecaptchaEnabled = server.config.Recaptcha.Enabled
|
data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled
|
||||||
data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey
|
data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey
|
||||||
data.HcaptchaEnabled = server.config.Hcaptcha.Enabled
|
data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled
|
||||||
data.HcaptchaSiteKey = server.config.Hcaptcha.SiteKey
|
data.RegistrationHcaptchaSiteKey = server.config.Captcha.Registration.Hcaptcha.SiteKey
|
||||||
|
data.LoginRecaptchaEnabled = server.config.Captcha.Login.Recaptcha.Enabled
|
||||||
|
data.LoginRecaptchaSiteKey = server.config.Captcha.Login.Recaptcha.SiteKey
|
||||||
|
data.LoginHcaptchaEnabled = server.config.Captcha.Login.Hcaptcha.Enabled
|
||||||
|
data.LoginHcaptchaSiteKey = server.config.Captcha.Login.Hcaptcha.SiteKey
|
||||||
data.NewProjectDashboard = server.config.NewProjectDashboard
|
data.NewProjectDashboard = server.config.NewProjectDashboard
|
||||||
data.NewObjectsFlow = server.config.NewObjectsFlow
|
data.NewObjectsFlow = server.config.NewObjectsFlow
|
||||||
data.NewAccessGrantFlow = server.config.NewAccessGrantFlow
|
data.NewAccessGrantFlow = server.config.NewAccessGrantFlow
|
||||||
|
@ -119,18 +119,19 @@ var (
|
|||||||
//
|
//
|
||||||
// architecture: Service
|
// architecture: Service
|
||||||
type Service struct {
|
type Service struct {
|
||||||
log, auditLogger *zap.Logger
|
log, auditLogger *zap.Logger
|
||||||
store DB
|
store DB
|
||||||
restKeys RESTKeys
|
restKeys RESTKeys
|
||||||
projectAccounting accounting.ProjectAccounting
|
projectAccounting accounting.ProjectAccounting
|
||||||
projectUsage *accounting.Service
|
projectUsage *accounting.Service
|
||||||
buckets Buckets
|
buckets Buckets
|
||||||
partners *rewards.PartnersService
|
partners *rewards.PartnersService
|
||||||
accounts payments.Accounts
|
accounts payments.Accounts
|
||||||
depositWallets payments.DepositWallets
|
depositWallets payments.DepositWallets
|
||||||
captchaHandler CaptchaHandler
|
registrationCaptchaHandler CaptchaHandler
|
||||||
analytics *analytics.Service
|
loginCaptchaHandler CaptchaHandler
|
||||||
tokens *consoleauth.Service
|
analytics *analytics.Service
|
||||||
|
tokens *consoleauth.Service
|
||||||
|
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
@ -157,22 +158,26 @@ type Config struct {
|
|||||||
FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"`
|
FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"`
|
||||||
SessionDuration time.Duration `help:"duration a session is valid for" default:"168h"`
|
SessionDuration time.Duration `help:"duration a session is valid for" default:"168h"`
|
||||||
UsageLimits UsageLimitsConfig
|
UsageLimits UsageLimitsConfig
|
||||||
Recaptcha RecaptchaConfig
|
Captcha CaptchaConfig
|
||||||
Hcaptcha HcaptchaConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecaptchaConfig contains configurations for the reCAPTCHA system.
|
// CaptchaConfig contains configurations for login/registration captcha system.
|
||||||
type RecaptchaConfig struct {
|
type CaptchaConfig struct {
|
||||||
Enabled bool `help:"whether or not reCAPTCHA is enabled for user registration" default:"false"`
|
Login MultiCaptchaConfig
|
||||||
SiteKey string `help:"reCAPTCHA site key"`
|
Registration MultiCaptchaConfig
|
||||||
SecretKey string `help:"reCAPTCHA secret key"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HcaptchaConfig contains configurations for the hCaptcha system.
|
// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems.
|
||||||
type HcaptchaConfig struct {
|
type MultiCaptchaConfig struct {
|
||||||
Enabled bool `help:"whether or not hCaptcha is enabled for user registration" default:"false"`
|
Recaptcha SingleCaptchaConfig
|
||||||
SiteKey string `help:"hCaptcha site key" default:""`
|
Hcaptcha SingleCaptchaConfig
|
||||||
SecretKey string `help:"hCaptcha secret key"`
|
}
|
||||||
|
|
||||||
|
// SingleCaptchaConfig contains configurations abstract captcha system.
|
||||||
|
type SingleCaptchaConfig struct {
|
||||||
|
Enabled bool `help:"whether or not captcha is enabled" default:"false"`
|
||||||
|
SiteKey string `help:"captcha site key"`
|
||||||
|
SecretKey string `help:"captcha secret key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payments separates all payment related functionality.
|
// Payments separates all payment related functionality.
|
||||||
@ -192,28 +197,39 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting
|
|||||||
config.PasswordCost = bcrypt.DefaultCost
|
config.PasswordCost = bcrypt.DefaultCost
|
||||||
}
|
}
|
||||||
|
|
||||||
var captchaHandler CaptchaHandler
|
// We have two separate captcha handlers for login and registration.
|
||||||
if config.Recaptcha.Enabled {
|
// We want to easily swap between captchas independently.
|
||||||
captchaHandler = NewDefaultCaptcha(Recaptcha, config.Recaptcha.SecretKey)
|
// For example, google recaptcha for login screen and hcaptcha for registration screen.
|
||||||
} else if config.Hcaptcha.Enabled {
|
var registrationCaptchaHandler CaptchaHandler
|
||||||
captchaHandler = NewDefaultCaptcha(Hcaptcha, config.Hcaptcha.SecretKey)
|
if config.Captcha.Registration.Recaptcha.Enabled {
|
||||||
|
registrationCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Registration.Recaptcha.SecretKey)
|
||||||
|
} else if config.Captcha.Registration.Hcaptcha.Enabled {
|
||||||
|
registrationCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Registration.Hcaptcha.SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginCaptchaHandler CaptchaHandler
|
||||||
|
if config.Captcha.Login.Recaptcha.Enabled {
|
||||||
|
loginCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Login.Recaptcha.SecretKey)
|
||||||
|
} else if config.Captcha.Login.Hcaptcha.Enabled {
|
||||||
|
loginCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Login.Hcaptcha.SecretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
log: log,
|
log: log,
|
||||||
auditLogger: log.Named("auditlog"),
|
auditLogger: log.Named("auditlog"),
|
||||||
store: store,
|
store: store,
|
||||||
restKeys: restKeys,
|
restKeys: restKeys,
|
||||||
projectAccounting: projectAccounting,
|
projectAccounting: projectAccounting,
|
||||||
projectUsage: projectUsage,
|
projectUsage: projectUsage,
|
||||||
buckets: buckets,
|
buckets: buckets,
|
||||||
partners: partners,
|
partners: partners,
|
||||||
accounts: accounts,
|
accounts: accounts,
|
||||||
depositWallets: depositWallets,
|
depositWallets: depositWallets,
|
||||||
captchaHandler: captchaHandler,
|
registrationCaptchaHandler: registrationCaptchaHandler,
|
||||||
analytics: analytics,
|
loginCaptchaHandler: loginCaptchaHandler,
|
||||||
tokens: tokens,
|
analytics: analytics,
|
||||||
config: config,
|
tokens: tokens,
|
||||||
|
config: config,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,8 +647,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
|
|||||||
|
|
||||||
mon.Counter("create_user_attempt").Inc(1) //mon:locked
|
mon.Counter("create_user_attempt").Inc(1) //mon:locked
|
||||||
|
|
||||||
if s.config.Recaptcha.Enabled || s.config.Hcaptcha.Enabled {
|
if s.config.Captcha.Registration.Recaptcha.Enabled || s.config.Captcha.Registration.Hcaptcha.Enabled {
|
||||||
valid, err := s.captchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
|
valid, err := s.registrationCaptchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mon.Counter("create_user_captcha_error").Inc(1) //mon:locked
|
mon.Counter("create_user_captcha_error").Inc(1) //mon:locked
|
||||||
s.log.Error("captcha authorization failed", zap.Error(err))
|
s.log.Error("captcha authorization failed", zap.Error(err))
|
||||||
@ -738,7 +754,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
|
|||||||
// TestSwapCaptchaHandler replaces the existing handler for captchas with
|
// TestSwapCaptchaHandler replaces the existing handler for captchas with
|
||||||
// the one specified for use in testing.
|
// the one specified for use in testing.
|
||||||
func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) {
|
func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) {
|
||||||
s.captchaHandler = h
|
s.registrationCaptchaHandler = h
|
||||||
|
s.loginCaptchaHandler = h
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateActivationToken - is a method for generating activation token.
|
// GenerateActivationToken - is a method for generating activation token.
|
||||||
@ -946,6 +963,19 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token consoleaut
|
|||||||
|
|
||||||
mon.Counter("login_attempt").Inc(1) //mon:locked
|
mon.Counter("login_attempt").Inc(1) //mon:locked
|
||||||
|
|
||||||
|
if s.config.Captcha.Login.Recaptcha.Enabled || s.config.Captcha.Login.Hcaptcha.Enabled {
|
||||||
|
valid, err := s.loginCaptchaHandler.Verify(ctx, request.CaptchaResponse, request.IP)
|
||||||
|
if err != nil {
|
||||||
|
mon.Counter("login_user_captcha_error").Inc(1) //mon:locked
|
||||||
|
s.log.Error("captcha authorization failed", zap.Error(err))
|
||||||
|
return consoleauth.Token{}, ErrCaptcha.Wrap(err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
mon.Counter("login_user_captcha_unsuccessful").Inc(1) //mon:locked
|
||||||
|
return consoleauth.Token{}, ErrCaptcha.New("captcha validation unsuccessful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email)
|
user, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
if len(unverified) > 0 {
|
if len(unverified) > 0 {
|
||||||
|
@ -117,6 +117,7 @@ type AuthUser struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
MFAPasscode string `json:"mfaPasscode"`
|
MFAPasscode string `json:"mfaPasscode"`
|
||||||
MFARecoveryCode string `json:"mfaRecoveryCode"`
|
MFARecoveryCode string `json:"mfaRecoveryCode"`
|
||||||
|
CaptchaResponse string `json:"captchaResponse"`
|
||||||
IP string `json:"-"`
|
IP string `json:"-"`
|
||||||
UserAgent string `json:"-"`
|
UserAgent string `json:"-"`
|
||||||
}
|
}
|
||||||
|
54
scripts/testdata/satellite-config.yaml.lock
vendored
54
scripts/testdata/satellite-config.yaml.lock
vendored
@ -109,6 +109,42 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
|||||||
# url link for for beta satellite support
|
# url link for for beta satellite support
|
||||||
# console.beta-satellite-support-url: ""
|
# console.beta-satellite-support-url: ""
|
||||||
|
|
||||||
|
# whether or not captcha is enabled
|
||||||
|
# console.captcha.login.hcaptcha.enabled: false
|
||||||
|
|
||||||
|
# captcha secret key
|
||||||
|
# console.captcha.login.hcaptcha.secret-key: ""
|
||||||
|
|
||||||
|
# captcha site key
|
||||||
|
# console.captcha.login.hcaptcha.site-key: ""
|
||||||
|
|
||||||
|
# whether or not captcha is enabled
|
||||||
|
# console.captcha.login.recaptcha.enabled: false
|
||||||
|
|
||||||
|
# captcha secret key
|
||||||
|
# console.captcha.login.recaptcha.secret-key: ""
|
||||||
|
|
||||||
|
# captcha site key
|
||||||
|
# console.captcha.login.recaptcha.site-key: ""
|
||||||
|
|
||||||
|
# whether or not captcha is enabled
|
||||||
|
# console.captcha.registration.hcaptcha.enabled: false
|
||||||
|
|
||||||
|
# captcha secret key
|
||||||
|
# console.captcha.registration.hcaptcha.secret-key: ""
|
||||||
|
|
||||||
|
# captcha site key
|
||||||
|
# console.captcha.registration.hcaptcha.site-key: ""
|
||||||
|
|
||||||
|
# whether or not captcha is enabled
|
||||||
|
# console.captcha.registration.recaptcha.enabled: false
|
||||||
|
|
||||||
|
# captcha secret key
|
||||||
|
# console.captcha.registration.recaptcha.secret-key: ""
|
||||||
|
|
||||||
|
# captcha site key
|
||||||
|
# console.captcha.registration.recaptcha.site-key: ""
|
||||||
|
|
||||||
# url link to contacts page
|
# url link to contacts page
|
||||||
# console.contact-info-url: https://forum.storj.io
|
# console.contact-info-url: https://forum.storj.io
|
||||||
|
|
||||||
@ -148,15 +184,6 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
|||||||
# indicates if generated console api should be used
|
# indicates if generated console api should be used
|
||||||
# console.generated-api-enabled: false
|
# console.generated-api-enabled: false
|
||||||
|
|
||||||
# whether or not hCaptcha is enabled for user registration
|
|
||||||
# console.hcaptcha.enabled: false
|
|
||||||
|
|
||||||
# hCaptcha secret key
|
|
||||||
# console.hcaptcha.secret-key: ""
|
|
||||||
|
|
||||||
# hCaptcha site key
|
|
||||||
# console.hcaptcha.site-key: ""
|
|
||||||
|
|
||||||
# url link to storj.io homepage
|
# url link to storj.io homepage
|
||||||
# console.homepage-url: https://www.storj.io
|
# console.homepage-url: https://www.storj.io
|
||||||
|
|
||||||
@ -226,15 +253,6 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
|||||||
# number of clients whose rate limits we store
|
# number of clients whose rate limits we store
|
||||||
# console.rate-limit.num-limits: 1000
|
# console.rate-limit.num-limits: 1000
|
||||||
|
|
||||||
# whether or not reCAPTCHA is enabled for user registration
|
|
||||||
# console.recaptcha.enabled: false
|
|
||||||
|
|
||||||
# reCAPTCHA secret key
|
|
||||||
# console.recaptcha.secret-key: ""
|
|
||||||
|
|
||||||
# reCAPTCHA site key
|
|
||||||
# console.recaptcha.site-key: ""
|
|
||||||
|
|
||||||
# used to display at web satellite console
|
# used to display at web satellite console
|
||||||
# console.satellite-name: Storj
|
# console.satellite-name: Storj
|
||||||
|
|
||||||
|
@ -1,47 +1,51 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head lang="en">
|
<head lang="en">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="external-address" content="{{ .ExternalAddress }}">
|
<meta name="external-address" content="{{ .ExternalAddress }}">
|
||||||
<meta name="satellite-name" content="{{ .SatelliteName }}">
|
<meta name="satellite-name" content="{{ .SatelliteName }}">
|
||||||
<meta name="satellite-nodeurl" content="{{ .SatelliteNodeURL }}">
|
<meta name="satellite-nodeurl" content="{{ .SatelliteNodeURL }}">
|
||||||
<meta name="stripe-public-key" content="{{ .StripePublicKey }}">
|
<meta name="stripe-public-key" content="{{ .StripePublicKey }}">
|
||||||
<meta name="partnered-satellites" content="{{ .PartneredSatellites }}">
|
<meta name="partnered-satellites" content="{{ .PartneredSatellites }}">
|
||||||
<meta name="default-project-limit" content="{{ .DefaultProjectLimit }}">
|
<meta name="default-project-limit" content="{{ .DefaultProjectLimit }}">
|
||||||
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
|
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
|
||||||
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
|
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
|
||||||
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
|
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
|
||||||
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
|
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
|
||||||
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
|
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
|
||||||
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
|
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
|
||||||
<meta name="documentation-url" content="{{ .DocumentationURL }}">
|
<meta name="documentation-url" content="{{ .DocumentationURL }}">
|
||||||
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
|
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
|
||||||
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
|
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
|
||||||
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
|
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
|
||||||
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
|
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
|
||||||
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
|
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
|
||||||
<meta name="egress-tb-price" content="{{ .EgressTBPrice }}">
|
<meta name="egress-tb-price" content="{{ .EgressTBPrice }}">
|
||||||
<meta name="segment-price" content="{{ .SegmentPrice }}">
|
<meta name="segment-price" content="{{ .SegmentPrice }}">
|
||||||
<meta name="recaptcha-enabled" content="{{ .RecaptchaEnabled }}">
|
<meta name="registration-recaptcha-enabled" content="{{ .RegistrationRecaptchaEnabled }}">
|
||||||
<meta name="recaptcha-site-key" content="{{ .RecaptchaSiteKey }}">
|
<meta name="registration-recaptcha-site-key" content="{{ .RegistrationRecaptchaSiteKey }}">
|
||||||
<meta name="hcaptcha-enabled" content="{{ .HcaptchaEnabled }}">
|
<meta name="registration-hcaptcha-enabled" content="{{ .RegistrationHcaptchaEnabled }}">
|
||||||
<meta name="hcaptcha-site-key" content="{{ .HcaptchaSiteKey }}">
|
<meta name="registration-hcaptcha-site-key" content="{{ .RegistrationHcaptchaSiteKey }}">
|
||||||
<meta name="new-project-dashboard" content="{{ .NewProjectDashboard }}">
|
<meta name="login-recaptcha-enabled" content="{{ .LoginRecaptchaEnabled }}">
|
||||||
<meta name="default-paid-storage-limit" content="{{ .DefaultPaidStorageLimit }}">
|
<meta name="login-recaptcha-site-key" content="{{ .LoginRecaptchaSiteKey }}">
|
||||||
<meta name="default-paid-bandwidth-limit" content="{{ .DefaultPaidBandwidthLimit }}">
|
<meta name="login-hcaptcha-enabled" content="{{ .LoginHcaptchaEnabled }}">
|
||||||
<meta name="new-objects-flow" content="{{ .NewObjectsFlow }}">
|
<meta name="login-hcaptcha-site-key" content="{{ .LoginHcaptchaSiteKey }}">
|
||||||
<meta name="new-access-grant-flow" content="{{ .NewAccessGrantFlow }}">
|
<meta name="new-project-dashboard" content="{{ .NewProjectDashboard }}">
|
||||||
<meta name="new-billing-screen" content="{{ .NewBillingScreen }}">
|
<meta name="default-paid-storage-limit" content="{{ .DefaultPaidStorageLimit }}">
|
||||||
<meta name="inactivity-timer-enabled" content="{{ .InactivityTimerEnabled }}">
|
<meta name="default-paid-bandwidth-limit" content="{{ .DefaultPaidBandwidthLimit }}">
|
||||||
<meta name="inactivity-timer-delay" content="{{ .InactivityTimerDelay }}">
|
<meta name="new-objects-flow" content="{{ .NewObjectsFlow }}">
|
||||||
<meta name="optional-signup-success-url" content="{{ .OptionalSignupSuccessURL }}">
|
<meta name="new-access-grant-flow" content="{{ .NewAccessGrantFlow }}">
|
||||||
<meta name="homepage-url" content="{{ .HomepageURL }}">
|
<meta name="new-billing-screen" content="{{ .NewBillingScreen }}">
|
||||||
<title>{{ .SatelliteName }}</title>
|
<meta name="inactivity-timer-enabled" content="{{ .InactivityTimerEnabled }}">
|
||||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
<meta name="inactivity-timer-delay" content="{{ .InactivityTimerDelay }}">
|
||||||
<link rel="dns-prefetch" href="https://js.stripe.com">
|
<meta name="optional-signup-success-url" content="{{ .OptionalSignupSuccessURL }}">
|
||||||
</head>
|
<meta name="homepage-url" content="{{ .HomepageURL }}">
|
||||||
<body>
|
<title>{{ .SatelliteName }}</title>
|
||||||
<div id="app"></div>
|
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
||||||
</body>
|
<link rel="dns-prefetch" href="https://js.stripe.com">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -42,15 +42,17 @@ export class AuthHttpApi implements UsersApi {
|
|||||||
*
|
*
|
||||||
* @param email - email of the user
|
* @param email - email of the user
|
||||||
* @param password - password of the user
|
* @param password - password of the user
|
||||||
|
* @param captchaResponse - captcha response token
|
||||||
* @param mfaPasscode - MFA passcode
|
* @param mfaPasscode - MFA passcode
|
||||||
* @param mfaRecoveryCode - MFA recovery code
|
* @param mfaRecoveryCode - MFA recovery code
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public async token(email: string, password: string, mfaPasscode: string, mfaRecoveryCode: string): Promise<string> {
|
public async token(email: string, password: string, captchaResponse: string, mfaPasscode: string, mfaRecoveryCode: string): Promise<string> {
|
||||||
const path = `${this.ROOT_PATH}/token`;
|
const path = `${this.ROOT_PATH}/token`;
|
||||||
const body = {
|
const body = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
captchaResponse,
|
||||||
mfaPasscode: mfaPasscode || null,
|
mfaPasscode: mfaPasscode || null,
|
||||||
mfaRecoveryCode: mfaRecoveryCode || null,
|
mfaRecoveryCode: mfaRecoveryCode || null,
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-area" @keyup.enter="onLogin">
|
<div class="login-area" @keyup.enter="onLoginClick">
|
||||||
<div class="login-area__logo-wrapper">
|
<div class="login-area__logo-wrapper">
|
||||||
<LogoIcon class="logo" @click="onLogoClick" />
|
<LogoIcon class="logo" @click="onLogoClick" />
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +80,47 @@
|
|||||||
Or use recovery code
|
Or use recovery code
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<p class="login-area__content-area__container__button" :class="{ 'disabled-button': isLoading }" @click.prevent="onLogin">Sign In</p>
|
<div v-if="recaptchaEnabled" class="login-area__content-area__container__captcha-wrapper">
|
||||||
|
<div v-if="captchaError" class="login-area__content-area__container__captcha-wrapper__label-container">
|
||||||
|
<ErrorIcon />
|
||||||
|
<p class="login-area__content-area__container__captcha-wrapper__label-container__error">reCAPTCHA is required</p>
|
||||||
|
</div>
|
||||||
|
<VueRecaptcha
|
||||||
|
ref="recaptcha"
|
||||||
|
:sitekey="recaptchaSiteKey"
|
||||||
|
:load-recaptcha-script="true"
|
||||||
|
size="invisible"
|
||||||
|
@verify="onCaptchaVerified"
|
||||||
|
@expired="onCaptchaError"
|
||||||
|
@error="onCaptchaError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hcaptchaEnabled" class="login-area__content-area__container__captcha-wrapper">
|
||||||
|
<div v-if="captchaError" class="login-area__content-area__container__captcha-wrapper__label-container">
|
||||||
|
<ErrorIcon />
|
||||||
|
<p class="login-area__content-area__container__captcha-wrapper__label-container__error">HCaptcha is required</p>
|
||||||
|
</div>
|
||||||
|
<VueHcaptcha
|
||||||
|
ref="hcaptcha"
|
||||||
|
:sitekey="hcaptchaSiteKey"
|
||||||
|
:re-captcha-compat="false"
|
||||||
|
size="invisible"
|
||||||
|
@verify="onCaptchaVerified"
|
||||||
|
@expired="onCaptchaError"
|
||||||
|
@error="onCaptchaError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<v-button
|
||||||
|
class="login-area__content-area__container__button"
|
||||||
|
width="100%"
|
||||||
|
height="48px"
|
||||||
|
label="Sign In"
|
||||||
|
border-radius="50px"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:on-press="onLoginClick"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</v-button>
|
||||||
<span v-if="isMFARequired" class="login-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
|
<span v-if="isMFARequired" class="login-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
@ -100,15 +140,19 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import VueRecaptcha from "vue-recaptcha";
|
||||||
|
import VueHcaptcha from "@hcaptcha/vue-hcaptcha";
|
||||||
|
|
||||||
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
|
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
|
||||||
import VInput from '@/components/common/VInput.vue';
|
import VInput from '@/components/common/VInput.vue';
|
||||||
|
import VButton from '@/components/common/VButton.vue';
|
||||||
|
|
||||||
import WarningIcon from '@/../static/images/accessGrants/warning.svg';
|
import WarningIcon from '@/../static/images/accessGrants/warning.svg';
|
||||||
import GreyWarningIcon from '@/../static/images/common/greyWarning.svg';
|
import GreyWarningIcon from '@/../static/images/common/greyWarning.svg';
|
||||||
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
|
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
|
||||||
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
|
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
|
||||||
import LogoIcon from '@/../static/images/logo.svg';
|
import LogoIcon from '@/../static/images/logo.svg';
|
||||||
|
import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
|
||||||
|
|
||||||
import { AuthHttpApi } from '@/api/auth';
|
import { AuthHttpApi } from '@/api/auth';
|
||||||
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
|
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
|
||||||
@ -120,7 +164,6 @@ import { Validator } from '@/utils/validation';
|
|||||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||||
import { ErrorBadRequest } from "@/api/errors/ErrorBadRequest";
|
import { ErrorBadRequest } from "@/api/errors/ErrorBadRequest";
|
||||||
import { MetaUtils } from '@/utils/meta';
|
import { MetaUtils } from '@/utils/meta';
|
||||||
|
|
||||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
|
|
||||||
interface ClearInput {
|
interface ClearInput {
|
||||||
@ -131,12 +174,16 @@ interface ClearInput {
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
VInput,
|
VInput,
|
||||||
|
VButton,
|
||||||
BottomArrowIcon,
|
BottomArrowIcon,
|
||||||
SelectedCheckIcon,
|
SelectedCheckIcon,
|
||||||
LogoIcon,
|
LogoIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
GreyWarningIcon,
|
GreyWarningIcon,
|
||||||
|
ErrorIcon,
|
||||||
ConfirmMFAInput,
|
ConfirmMFAInput,
|
||||||
|
VueRecaptcha,
|
||||||
|
VueHcaptcha,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class Login extends Vue {
|
export default class Login extends Vue {
|
||||||
@ -147,6 +194,13 @@ export default class Login extends Vue {
|
|||||||
private isLoading = false;
|
private isLoading = false;
|
||||||
private emailError = '';
|
private emailError = '';
|
||||||
private passwordError = '';
|
private passwordError = '';
|
||||||
|
private captchaError = false;
|
||||||
|
private captchaResponseToken = '';
|
||||||
|
|
||||||
|
private readonly recaptchaEnabled: boolean = MetaUtils.getMetaContent('login-recaptcha-enabled') === 'true';
|
||||||
|
private readonly recaptchaSiteKey: string = MetaUtils.getMetaContent('login-recaptcha-site-key');
|
||||||
|
private readonly hcaptchaEnabled: boolean = MetaUtils.getMetaContent('login-hcaptcha-enabled') === 'true';
|
||||||
|
private readonly hcaptchaSiteKey: string = MetaUtils.getMetaContent('login-hcaptcha-site-key');
|
||||||
|
|
||||||
private readonly auth: AuthHttpApi = new AuthHttpApi();
|
private readonly auth: AuthHttpApi = new AuthHttpApi();
|
||||||
|
|
||||||
@ -168,6 +222,8 @@ export default class Login extends Vue {
|
|||||||
public readonly activatePath: string = RouteConfig.Activate.path;
|
public readonly activatePath: string = RouteConfig.Activate.path;
|
||||||
|
|
||||||
public $refs!: {
|
public $refs!: {
|
||||||
|
recaptcha: VueRecaptcha;
|
||||||
|
hcaptcha: VueHcaptcha;
|
||||||
mfaInput: ConfirmMFAInput & ClearInput;
|
mfaInput: ConfirmMFAInput & ClearInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -272,16 +328,48 @@ export default class Login extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs login action.
|
* Handles captcha verification response.
|
||||||
* Then changes location to project dashboard page.
|
|
||||||
*/
|
*/
|
||||||
public async onLogin(): Promise<void> {
|
public onCaptchaVerified(response: string): void {
|
||||||
|
this.captchaResponseToken = response;
|
||||||
|
this.captchaError = false;
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles captcha error and expiry.
|
||||||
|
*/
|
||||||
|
public onCaptchaError(): void {
|
||||||
|
this.captchaResponseToken = '';
|
||||||
|
this.captchaError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds on login button click logic.
|
||||||
|
*/
|
||||||
|
public async onLoginClick(): Promise<void> {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
if (this.$refs.recaptcha && !this.captchaResponseToken) {
|
||||||
|
this.$refs.recaptcha.execute();
|
||||||
|
return;
|
||||||
|
} if (this.$refs.hcaptcha && !this.captchaResponseToken) {
|
||||||
|
this.$refs.hcaptcha.execute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs login action.
|
||||||
|
* Then changes location to project dashboard page.
|
||||||
|
*/
|
||||||
|
public async login(): Promise<void> {
|
||||||
if (!this.validateFields()) {
|
if (!this.validateFields()) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
@ -289,8 +377,17 @@ export default class Login extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.auth.token(this.email, this.password, this.passcode, this.recoveryCode);
|
await this.auth.token(this.email, this.password, this.captchaResponseToken, this.passcode, this.recoveryCode);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (this.$refs.recaptcha) {
|
||||||
|
this.$refs.recaptcha.reset();
|
||||||
|
this.captchaResponseToken = '';
|
||||||
|
}
|
||||||
|
if (this.$refs.hcaptcha) {
|
||||||
|
this.$refs.hcaptcha.reset();
|
||||||
|
this.captchaResponseToken = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof ErrorMFARequired) {
|
if (error instanceof ErrorMFARequired) {
|
||||||
if (this.isMFARequired) this.isMFAError = true;
|
if (this.isMFARequired) this.isMFAError = true;
|
||||||
|
|
||||||
@ -495,25 +592,24 @@ export default class Login extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__button {
|
&__captcha-wrapper__label-container {
|
||||||
font-family: 'font_regular', sans-serif;
|
margin-top: 30px;
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
background-color: #376fff;
|
padding-bottom: 8px;
|
||||||
border-radius: 50px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
&:hover {
|
&__error {
|
||||||
background-color: #0059d0;
|
font-size: 16px;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #ff5560;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
&__cancel {
|
&__cancel {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -546,17 +642,11 @@ export default class Login extends Vue {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled,
|
.disabled {
|
||||||
.disabled-button {
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: #acb0bc;
|
color: #acb0bc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-button {
|
|
||||||
background-color: #dadde5;
|
|
||||||
border-color: #dadde5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: #376fff;
|
color: #376fff;
|
||||||
font-family: 'font_medium', sans-serif;
|
font-family: 'font_medium', sans-serif;
|
||||||
@ -594,6 +684,10 @@ export default class Login extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .grecaptcha-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 750px) {
|
||||||
|
|
||||||
.login-area {
|
.login-area {
|
||||||
|
@ -188,7 +188,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isProfessional" class="register-area__input-area__container__checkbox-area">
|
<div v-if="isProfessional" class="register-area__input-area__container__checkbox-area">
|
||||||
<label class="container">
|
<label class="checkmark-container">
|
||||||
<input id="sales" v-model="haveSalesContact" type="checkbox">
|
<input id="sales" v-model="haveSalesContact" type="checkbox">
|
||||||
<span class="checkmark" />
|
<span class="checkmark" />
|
||||||
</label>
|
</label>
|
||||||
@ -199,7 +199,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="register-area__input-area__container__checkbox-area">
|
<div class="register-area__input-area__container__checkbox-area">
|
||||||
<label class="container">
|
<label class="checkmark-container">
|
||||||
<input id="terms" v-model="isTermsAccepted" type="checkbox">
|
<input id="terms" v-model="isTermsAccepted" type="checkbox">
|
||||||
<span class="checkmark" :class="{'error': isTermsAcceptedError}" />
|
<span class="checkmark" :class="{'error': isTermsAcceptedError}" />
|
||||||
</label>
|
</label>
|
||||||
@ -220,7 +220,7 @@
|
|||||||
<VueRecaptcha
|
<VueRecaptcha
|
||||||
ref="recaptcha"
|
ref="recaptcha"
|
||||||
:sitekey="recaptchaSiteKey"
|
:sitekey="recaptchaSiteKey"
|
||||||
load-recaptcha-script="true"
|
:load-recaptcha-script="true"
|
||||||
size="invisible"
|
size="invisible"
|
||||||
@verify="onCaptchaVerified"
|
@verify="onCaptchaVerified"
|
||||||
@expired="onCaptchaError"
|
@expired="onCaptchaError"
|
||||||
@ -235,13 +235,24 @@
|
|||||||
<VueHcaptcha
|
<VueHcaptcha
|
||||||
ref="hcaptcha"
|
ref="hcaptcha"
|
||||||
:sitekey="hcaptchaSiteKey"
|
:sitekey="hcaptchaSiteKey"
|
||||||
|
:re-captcha-compat="false"
|
||||||
size="invisible"
|
size="invisible"
|
||||||
@verify="onCaptchaVerified"
|
@verify="onCaptchaVerified"
|
||||||
@expired="onCaptchaError"
|
@expired="onCaptchaError"
|
||||||
@error="onCaptchaError"
|
@error="onCaptchaError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="register-area__input-area__container__button" @click.prevent="onCreateClick">{{ viewConfig.signupButtonLabel }}</p>
|
<v-button
|
||||||
|
class="register-area__input-area__container__button"
|
||||||
|
width="100%"
|
||||||
|
height="48px"
|
||||||
|
:label="viewConfig.signupButtonLabel"
|
||||||
|
border-radius="50px"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:on-press="onCreateClick"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</v-button>
|
||||||
<div class="register-area__input-area__login-container">
|
<div class="register-area__input-area__login-container">
|
||||||
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
|
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
|
||||||
</div>
|
</div>
|
||||||
@ -258,6 +269,7 @@ import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
|||||||
|
|
||||||
import AddCouponCodeInput from '@/components/common/AddCouponCodeInput.vue';
|
import AddCouponCodeInput from '@/components/common/AddCouponCodeInput.vue';
|
||||||
import VInput from '@/components/common/VInput.vue';
|
import VInput from '@/components/common/VInput.vue';
|
||||||
|
import VButton from '@/components/common/VButton.vue';
|
||||||
import PasswordStrength from '@/components/common/PasswordStrength.vue';
|
import PasswordStrength from '@/components/common/PasswordStrength.vue';
|
||||||
import SelectInput from '@/components/common/SelectInput.vue';
|
import SelectInput from '@/components/common/SelectInput.vue';
|
||||||
|
|
||||||
@ -275,7 +287,6 @@ import {PartneredSatellite} from '@/types/common';
|
|||||||
import {User} from '@/types/users';
|
import {User} from '@/types/users';
|
||||||
import {MetaUtils} from '@/utils/meta';
|
import {MetaUtils} from '@/utils/meta';
|
||||||
import {Validator} from '@/utils/validation';
|
import {Validator} from '@/utils/validation';
|
||||||
|
|
||||||
import {AnalyticsHttpApi} from '@/api/analytics';
|
import {AnalyticsHttpApi} from '@/api/analytics';
|
||||||
|
|
||||||
type ViewConfig = {
|
type ViewConfig = {
|
||||||
@ -293,6 +304,7 @@ type ViewConfig = {
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
VInput,
|
VInput,
|
||||||
|
VButton,
|
||||||
BottomArrowIcon,
|
BottomArrowIcon,
|
||||||
ErrorIcon,
|
ErrorIcon,
|
||||||
SelectedCheckIcon,
|
SelectedCheckIcon,
|
||||||
@ -339,10 +351,10 @@ export default class RegisterArea extends Vue {
|
|||||||
|
|
||||||
private readonly auth: AuthHttpApi = new AuthHttpApi();
|
private readonly auth: AuthHttpApi = new AuthHttpApi();
|
||||||
|
|
||||||
private readonly recaptchaEnabled: boolean = MetaUtils.getMetaContent('recaptcha-enabled') === 'true';
|
private readonly recaptchaEnabled: boolean = MetaUtils.getMetaContent('registration-recaptcha-enabled') === 'true';
|
||||||
private readonly recaptchaSiteKey: string = MetaUtils.getMetaContent('recaptcha-site-key');
|
private readonly recaptchaSiteKey: string = MetaUtils.getMetaContent('registration-recaptcha-site-key');
|
||||||
private readonly hcaptchaEnabled: boolean = MetaUtils.getMetaContent('hcaptcha-enabled') === 'true';
|
private readonly hcaptchaEnabled: boolean = MetaUtils.getMetaContent('registration-hcaptcha-enabled') === 'true';
|
||||||
private readonly hcaptchaSiteKey: string = MetaUtils.getMetaContent('hcaptcha-site-key');
|
private readonly hcaptchaSiteKey: string = MetaUtils.getMetaContent('registration-hcaptcha-site-key');
|
||||||
|
|
||||||
public isPasswordStrengthShown = false;
|
public isPasswordStrengthShown = false;
|
||||||
|
|
||||||
@ -418,6 +430,12 @@ export default class RegisterArea extends Vue {
|
|||||||
* Validates input fields and proceeds user creation.
|
* Validates input fields and proceeds user creation.
|
||||||
*/
|
*/
|
||||||
public async onCreateClick(): Promise<void> {
|
public async onCreateClick(): Promise<void> {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.$refs.recaptcha && !this.captchaResponseToken) {
|
if (this.$refs.recaptcha && !this.captchaResponseToken) {
|
||||||
this.$refs.recaptcha.execute();
|
this.$refs.recaptcha.execute();
|
||||||
return;
|
return;
|
||||||
@ -639,16 +657,10 @@ export default class RegisterArea extends Vue {
|
|||||||
* Creates user and toggles successful registration area visibility.
|
* Creates user and toggles successful registration area visibility.
|
||||||
*/
|
*/
|
||||||
private async createUser(): Promise<void> {
|
private async createUser(): Promise<void> {
|
||||||
if (this.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.validateFields()) {
|
if (!this.validateFields()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
this.user.isProfessional = this.isProfessional;
|
this.user.isProfessional = this.isProfessional;
|
||||||
this.user.haveSalesContact = this.haveSalesContact;
|
this.user.haveSalesContact = this.haveSalesContact;
|
||||||
|
|
||||||
@ -676,6 +688,7 @@ export default class RegisterArea extends Vue {
|
|||||||
}
|
}
|
||||||
await this.$notify.error(error.message);
|
await this.$notify.error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1036,39 +1049,20 @@ export default class RegisterArea extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
font-family: 'font_regular', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #376fff;
|
|
||||||
border-radius: 50px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 48px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #0059d0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__captcha-wrapper {
|
&__captcha-wrapper__label-container {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
&__label-container {
|
&__error {
|
||||||
display: flex;
|
font-size: 16px;
|
||||||
justify-content: flex-start;
|
margin-left: 10px;
|
||||||
align-items: flex-end;
|
color: #ff5560;
|
||||||
padding-bottom: 8px;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
&__error {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-left: 10px;
|
|
||||||
color: #ff5560;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1125,7 +1119,11 @@ export default class RegisterArea extends Vue {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.input-wrap {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark-container {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
@ -1137,7 +1135,7 @@ export default class RegisterArea extends Vue {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container input {
|
.checkmark-container input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -1155,11 +1153,11 @@ export default class RegisterArea extends Vue {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container:hover input ~ .checkmark {
|
.checkmark-container:hover input ~ .checkmark {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container input:checked ~ .checkmark {
|
.checkmark-container input:checked ~ .checkmark {
|
||||||
border: 2px solid #afb7c1;
|
border: 2px solid #afb7c1;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
@ -1174,7 +1172,7 @@ export default class RegisterArea extends Vue {
|
|||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .checkmark:after {
|
.checkmark-container .checkmark:after {
|
||||||
left: 7px;
|
left: 7px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
@ -1184,10 +1182,14 @@ export default class RegisterArea extends Vue {
|
|||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container input:checked ~ .checkmark:after {
|
.checkmark-container input:checked ~ .checkmark:after {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .grecaptcha-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1429px) {
|
@media screen and (max-width: 1429px) {
|
||||||
|
|
||||||
.register-area {
|
.register-area {
|
||||||
@ -1342,10 +1344,6 @@ export default class RegisterArea extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .grecaptcha-badge {
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
@media screen and (max-width: 450px) {
|
||||||
|
|
||||||
.register-area {
|
.register-area {
|
||||||
|
Loading…
Reference in New Issue
Block a user