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:
Vitalii 2022-07-21 16:31:38 +03:00 committed by Storj Robot
parent 799b159bba
commit ad37ea4518
10 changed files with 418 additions and 203 deletions

View File

@ -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

View File

@ -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))
})
}

View File

@ -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

View File

@ -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 {

View File

@ -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:"-"`
} }

View File

@ -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

View File

@ -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>

View File

@ -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,
}; };

View File

@ -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 {

View File

@ -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 {