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_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/contact."failed_dial" 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
}
// TestRecaptcha ensures the reCAPTCHA service is working properly.
func TestRecaptcha(t *testing.T) {
// TestRegistrationRecaptcha ensures that registration reCAPTCHA service is working properly.
func TestRegistrationRecaptcha(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.Recaptcha.Enabled = true
config.Console.Recaptcha.SecretKey = "mySecretKey"
config.Console.Recaptcha.SiteKey = "mySiteKey"
config.Console.Captcha.Registration.Recaptcha.Enabled = true
config.Console.Captcha.Registration.Recaptcha.SecretKey = "mySecretKey"
config.Console.Captcha.Registration.Recaptcha.SiteKey = "mySiteKey"
},
},
}, 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))
})
}
// 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
EgressTBPrice string
SegmentPrice string
RecaptchaEnabled bool
RecaptchaSiteKey string
HcaptchaEnabled bool
HcaptchaSiteKey string
RegistrationRecaptchaEnabled bool
RegistrationRecaptchaSiteKey string
RegistrationHcaptchaEnabled bool
RegistrationHcaptchaSiteKey string
LoginRecaptchaEnabled bool
LoginRecaptchaSiteKey string
LoginHcaptchaEnabled bool
LoginHcaptchaSiteKey string
NewProjectDashboard bool
DefaultPaidStorageLimit 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.EgressTBPrice = server.pricing.EgressTBPrice
data.SegmentPrice = server.pricing.SegmentPrice
data.RecaptchaEnabled = server.config.Recaptcha.Enabled
data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey
data.HcaptchaEnabled = server.config.Hcaptcha.Enabled
data.HcaptchaSiteKey = server.config.Hcaptcha.SiteKey
data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled
data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey
data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled
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.NewObjectsFlow = server.config.NewObjectsFlow
data.NewAccessGrantFlow = server.config.NewAccessGrantFlow

View File

@ -119,18 +119,19 @@ var (
//
// architecture: Service
type Service struct {
log, auditLogger *zap.Logger
store DB
restKeys RESTKeys
projectAccounting accounting.ProjectAccounting
projectUsage *accounting.Service
buckets Buckets
partners *rewards.PartnersService
accounts payments.Accounts
depositWallets payments.DepositWallets
captchaHandler CaptchaHandler
analytics *analytics.Service
tokens *consoleauth.Service
log, auditLogger *zap.Logger
store DB
restKeys RESTKeys
projectAccounting accounting.ProjectAccounting
projectUsage *accounting.Service
buckets Buckets
partners *rewards.PartnersService
accounts payments.Accounts
depositWallets payments.DepositWallets
registrationCaptchaHandler CaptchaHandler
loginCaptchaHandler CaptchaHandler
analytics *analytics.Service
tokens *consoleauth.Service
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"`
SessionDuration time.Duration `help:"duration a session is valid for" default:"168h"`
UsageLimits UsageLimitsConfig
Recaptcha RecaptchaConfig
Hcaptcha HcaptchaConfig
Captcha CaptchaConfig
}
// RecaptchaConfig contains configurations for the reCAPTCHA system.
type RecaptchaConfig struct {
Enabled bool `help:"whether or not reCAPTCHA is enabled for user registration" default:"false"`
SiteKey string `help:"reCAPTCHA site key"`
SecretKey string `help:"reCAPTCHA secret key"`
// CaptchaConfig contains configurations for login/registration captcha system.
type CaptchaConfig struct {
Login MultiCaptchaConfig
Registration MultiCaptchaConfig
}
// HcaptchaConfig contains configurations for the hCaptcha system.
type HcaptchaConfig struct {
Enabled bool `help:"whether or not hCaptcha is enabled for user registration" default:"false"`
SiteKey string `help:"hCaptcha site key" default:""`
SecretKey string `help:"hCaptcha secret key"`
// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems.
type MultiCaptchaConfig struct {
Recaptcha SingleCaptchaConfig
Hcaptcha SingleCaptchaConfig
}
// 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.
@ -192,28 +197,39 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting
config.PasswordCost = bcrypt.DefaultCost
}
var captchaHandler CaptchaHandler
if config.Recaptcha.Enabled {
captchaHandler = NewDefaultCaptcha(Recaptcha, config.Recaptcha.SecretKey)
} else if config.Hcaptcha.Enabled {
captchaHandler = NewDefaultCaptcha(Hcaptcha, config.Hcaptcha.SecretKey)
// We have two separate captcha handlers for login and registration.
// We want to easily swap between captchas independently.
// For example, google recaptcha for login screen and hcaptcha for registration screen.
var registrationCaptchaHandler CaptchaHandler
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{
log: log,
auditLogger: log.Named("auditlog"),
store: store,
restKeys: restKeys,
projectAccounting: projectAccounting,
projectUsage: projectUsage,
buckets: buckets,
partners: partners,
accounts: accounts,
depositWallets: depositWallets,
captchaHandler: captchaHandler,
analytics: analytics,
tokens: tokens,
config: config,
log: log,
auditLogger: log.Named("auditlog"),
store: store,
restKeys: restKeys,
projectAccounting: projectAccounting,
projectUsage: projectUsage,
buckets: buckets,
partners: partners,
accounts: accounts,
depositWallets: depositWallets,
registrationCaptchaHandler: registrationCaptchaHandler,
loginCaptchaHandler: loginCaptchaHandler,
analytics: analytics,
tokens: tokens,
config: config,
}, 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
if s.config.Recaptcha.Enabled || s.config.Hcaptcha.Enabled {
valid, err := s.captchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
if s.config.Captcha.Registration.Recaptcha.Enabled || s.config.Captcha.Registration.Hcaptcha.Enabled {
valid, err := s.registrationCaptchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
if err != nil {
mon.Counter("create_user_captcha_error").Inc(1) //mon:locked
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
// the one specified for use in testing.
func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) {
s.captchaHandler = h
s.registrationCaptchaHandler = h
s.loginCaptchaHandler = h
}
// 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
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)
if user == nil {
if len(unverified) > 0 {

View File

@ -117,6 +117,7 @@ type AuthUser struct {
Password string `json:"password"`
MFAPasscode string `json:"mfaPasscode"`
MFARecoveryCode string `json:"mfaRecoveryCode"`
CaptchaResponse string `json:"captchaResponse"`
IP 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
# 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
# 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
# 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
# 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
# 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
# console.satellite-name: Storj

View File

@ -1,47 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="external-address" content="{{ .ExternalAddress }}">
<meta name="satellite-name" content="{{ .SatelliteName }}">
<meta name="satellite-nodeurl" content="{{ .SatelliteNodeURL }}">
<meta name="stripe-public-key" content="{{ .StripePublicKey }}">
<meta name="partnered-satellites" content="{{ .PartneredSatellites }}">
<meta name="default-project-limit" content="{{ .DefaultProjectLimit }}">
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
<meta name="documentation-url" content="{{ .DocumentationURL }}">
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
<meta name="egress-tb-price" content="{{ .EgressTBPrice }}">
<meta name="segment-price" content="{{ .SegmentPrice }}">
<meta name="recaptcha-enabled" content="{{ .RecaptchaEnabled }}">
<meta name="recaptcha-site-key" content="{{ .RecaptchaSiteKey }}">
<meta name="hcaptcha-enabled" content="{{ .HcaptchaEnabled }}">
<meta name="hcaptcha-site-key" content="{{ .HcaptchaSiteKey }}">
<meta name="new-project-dashboard" content="{{ .NewProjectDashboard }}">
<meta name="default-paid-storage-limit" content="{{ .DefaultPaidStorageLimit }}">
<meta name="default-paid-bandwidth-limit" content="{{ .DefaultPaidBandwidthLimit }}">
<meta name="new-objects-flow" content="{{ .NewObjectsFlow }}">
<meta name="new-access-grant-flow" content="{{ .NewAccessGrantFlow }}">
<meta name="new-billing-screen" content="{{ .NewBillingScreen }}">
<meta name="inactivity-timer-enabled" content="{{ .InactivityTimerEnabled }}">
<meta name="inactivity-timer-delay" content="{{ .InactivityTimerDelay }}">
<meta name="optional-signup-success-url" content="{{ .OptionalSignupSuccessURL }}">
<meta name="homepage-url" content="{{ .HomepageURL }}">
<title>{{ .SatelliteName }}</title>
<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">
<link rel="dns-prefetch" href="https://js.stripe.com">
</head>
<body>
<div id="app"></div>
</body>
<head lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="external-address" content="{{ .ExternalAddress }}">
<meta name="satellite-name" content="{{ .SatelliteName }}">
<meta name="satellite-nodeurl" content="{{ .SatelliteNodeURL }}">
<meta name="stripe-public-key" content="{{ .StripePublicKey }}">
<meta name="partnered-satellites" content="{{ .PartneredSatellites }}">
<meta name="default-project-limit" content="{{ .DefaultProjectLimit }}">
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
<meta name="documentation-url" content="{{ .DocumentationURL }}">
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
<meta name="egress-tb-price" content="{{ .EgressTBPrice }}">
<meta name="segment-price" content="{{ .SegmentPrice }}">
<meta name="registration-recaptcha-enabled" content="{{ .RegistrationRecaptchaEnabled }}">
<meta name="registration-recaptcha-site-key" content="{{ .RegistrationRecaptchaSiteKey }}">
<meta name="registration-hcaptcha-enabled" content="{{ .RegistrationHcaptchaEnabled }}">
<meta name="registration-hcaptcha-site-key" content="{{ .RegistrationHcaptchaSiteKey }}">
<meta name="login-recaptcha-enabled" content="{{ .LoginRecaptchaEnabled }}">
<meta name="login-recaptcha-site-key" content="{{ .LoginRecaptchaSiteKey }}">
<meta name="login-hcaptcha-enabled" content="{{ .LoginHcaptchaEnabled }}">
<meta name="login-hcaptcha-site-key" content="{{ .LoginHcaptchaSiteKey }}">
<meta name="new-project-dashboard" content="{{ .NewProjectDashboard }}">
<meta name="default-paid-storage-limit" content="{{ .DefaultPaidStorageLimit }}">
<meta name="default-paid-bandwidth-limit" content="{{ .DefaultPaidBandwidthLimit }}">
<meta name="new-objects-flow" content="{{ .NewObjectsFlow }}">
<meta name="new-access-grant-flow" content="{{ .NewAccessGrantFlow }}">
<meta name="new-billing-screen" content="{{ .NewBillingScreen }}">
<meta name="inactivity-timer-enabled" content="{{ .InactivityTimerEnabled }}">
<meta name="inactivity-timer-delay" content="{{ .InactivityTimerDelay }}">
<meta name="optional-signup-success-url" content="{{ .OptionalSignupSuccessURL }}">
<meta name="homepage-url" content="{{ .HomepageURL }}">
<title>{{ .SatelliteName }}</title>
<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">
<link rel="dns-prefetch" href="https://js.stripe.com">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -42,15 +42,17 @@ export class AuthHttpApi implements UsersApi {
*
* @param email - email of the user
* @param password - password of the user
* @param captchaResponse - captcha response token
* @param mfaPasscode - MFA passcode
* @param mfaRecoveryCode - MFA recovery code
* @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 body = {
email,
password,
captchaResponse,
mfaPasscode: mfaPasscode || null,
mfaRecoveryCode: mfaRecoveryCode || null,
};

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
<template>
<div class="login-area" @keyup.enter="onLogin">
<div class="login-area" @keyup.enter="onLoginClick">
<div class="login-area__logo-wrapper">
<LogoIcon class="logo" @click="onLogoClick" />
</div>
@ -80,7 +80,47 @@
Or use recovery code
</span>
</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">
Cancel
</span>
@ -100,15 +140,19 @@
<script lang="ts">
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 VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import WarningIcon from '@/../static/images/accessGrants/warning.svg';
import GreyWarningIcon from '@/../static/images/common/greyWarning.svg';
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
import LogoIcon from '@/../static/images/logo.svg';
import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
import { AuthHttpApi } from '@/api/auth';
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
@ -120,7 +164,6 @@ import { Validator } from '@/utils/validation';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { ErrorBadRequest } from "@/api/errors/ErrorBadRequest";
import { MetaUtils } from '@/utils/meta';
import { AnalyticsHttpApi } from '@/api/analytics';
interface ClearInput {
@ -131,12 +174,16 @@ interface ClearInput {
@Component({
components: {
VInput,
VButton,
BottomArrowIcon,
SelectedCheckIcon,
LogoIcon,
WarningIcon,
GreyWarningIcon,
ErrorIcon,
ConfirmMFAInput,
VueRecaptcha,
VueHcaptcha,
},
})
export default class Login extends Vue {
@ -147,6 +194,13 @@ export default class Login extends Vue {
private isLoading = false;
private emailError = '';
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();
@ -168,6 +222,8 @@ export default class Login extends Vue {
public readonly activatePath: string = RouteConfig.Activate.path;
public $refs!: {
recaptcha: VueRecaptcha;
hcaptcha: VueHcaptcha;
mfaInput: ConfirmMFAInput & ClearInput;
};
@ -272,16 +328,48 @@ export default class Login extends Vue {
}
/**
* Performs login action.
* Then changes location to project dashboard page.
* Handles captcha verification response.
*/
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) {
return;
}
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()) {
this.isLoading = false;
@ -289,8 +377,17 @@ export default class Login extends Vue {
}
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) {
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 (this.isMFARequired) this.isMFAError = true;
@ -495,25 +592,24 @@ export default class Login extends Vue {
}
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
&__captcha-wrapper__label-container {
margin-top: 30px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
justify-content: flex-start;
align-items: flex-end;
padding-bottom: 8px;
&:hover {
background-color: #0059d0;
&__error {
font-size: 16px;
margin-left: 10px;
color: #ff5560;
}
}
&__button {
margin-top: 40px;
}
&__cancel {
align-self: center;
font-size: 16px;
@ -546,17 +642,11 @@ export default class Login extends Vue {
cursor: pointer;
}
.disabled,
.disabled-button {
.disabled {
pointer-events: none;
color: #acb0bc;
}
.disabled-button {
background-color: #dadde5;
border-color: #dadde5;
}
.link {
color: #376fff;
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) {
.login-area {

View File

@ -188,7 +188,7 @@
</p>
</div>
<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">
<span class="checkmark" />
</label>
@ -199,7 +199,7 @@
</label>
</div>
<div class="register-area__input-area__container__checkbox-area">
<label class="container">
<label class="checkmark-container">
<input id="terms" v-model="isTermsAccepted" type="checkbox">
<span class="checkmark" :class="{'error': isTermsAcceptedError}" />
</label>
@ -220,7 +220,7 @@
<VueRecaptcha
ref="recaptcha"
:sitekey="recaptchaSiteKey"
load-recaptcha-script="true"
:load-recaptcha-script="true"
size="invisible"
@verify="onCaptchaVerified"
@expired="onCaptchaError"
@ -235,13 +235,24 @@
<VueHcaptcha
ref="hcaptcha"
:sitekey="hcaptchaSiteKey"
:re-captcha-compat="false"
size="invisible"
@verify="onCaptchaVerified"
@expired="onCaptchaError"
@error="onCaptchaError"
/>
</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">
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
</div>
@ -258,6 +269,7 @@ import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
import AddCouponCodeInput from '@/components/common/AddCouponCodeInput.vue';
import VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import PasswordStrength from '@/components/common/PasswordStrength.vue';
import SelectInput from '@/components/common/SelectInput.vue';
@ -275,7 +287,6 @@ import {PartneredSatellite} from '@/types/common';
import {User} from '@/types/users';
import {MetaUtils} from '@/utils/meta';
import {Validator} from '@/utils/validation';
import {AnalyticsHttpApi} from '@/api/analytics';
type ViewConfig = {
@ -293,6 +304,7 @@ type ViewConfig = {
@Component({
components: {
VInput,
VButton,
BottomArrowIcon,
ErrorIcon,
SelectedCheckIcon,
@ -339,10 +351,10 @@ export default class RegisterArea extends Vue {
private readonly auth: AuthHttpApi = new AuthHttpApi();
private readonly recaptchaEnabled: boolean = MetaUtils.getMetaContent('recaptcha-enabled') === 'true';
private readonly recaptchaSiteKey: string = MetaUtils.getMetaContent('recaptcha-site-key');
private readonly hcaptchaEnabled: boolean = MetaUtils.getMetaContent('hcaptcha-enabled') === 'true';
private readonly hcaptchaSiteKey: string = MetaUtils.getMetaContent('hcaptcha-site-key');
private readonly recaptchaEnabled: boolean = MetaUtils.getMetaContent('registration-recaptcha-enabled') === 'true';
private readonly recaptchaSiteKey: string = MetaUtils.getMetaContent('registration-recaptcha-site-key');
private readonly hcaptchaEnabled: boolean = MetaUtils.getMetaContent('registration-hcaptcha-enabled') === 'true';
private readonly hcaptchaSiteKey: string = MetaUtils.getMetaContent('registration-hcaptcha-site-key');
public isPasswordStrengthShown = false;
@ -418,6 +430,12 @@ export default class RegisterArea extends Vue {
* Validates input fields and proceeds user creation.
*/
public async onCreateClick(): Promise<void> {
if (this.isLoading) {
return;
}
this.isLoading = true;
if (this.$refs.recaptcha && !this.captchaResponseToken) {
this.$refs.recaptcha.execute();
return;
@ -639,16 +657,10 @@ export default class RegisterArea extends Vue {
* Creates user and toggles successful registration area visibility.
*/
private async createUser(): Promise<void> {
if (this.isLoading) {
return;
}
if (!this.validateFields()) {
return;
}
this.isLoading = true;
this.user.isProfessional = this.isProfessional;
this.user.haveSalesContact = this.haveSalesContact;
@ -676,6 +688,7 @@ export default class RegisterArea extends Vue {
}
await this.$notify.error(error.message);
}
this.isLoading = false;
}
}
@ -1036,39 +1049,20 @@ export default class RegisterArea extends Vue {
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
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;
display: flex;
justify-content: flex-start;
align-items: flex-end;
padding-bottom: 8px;
&__label-container {
display: flex;
justify-content: flex-start;
align-items: flex-end;
padding-bottom: 8px;
flex-direction: row;
&__error {
font-size: 16px;
margin-left: 10px;
color: #ff5560;
}
&__error {
font-size: 16px;
margin-left: 10px;
color: #ff5560;
}
}
}
@ -1125,7 +1119,11 @@ export default class RegisterArea extends Vue {
width: 100%;
}
.container {
.input-wrap {
margin-top: 10px;
}
.checkmark-container {
display: block;
position: relative;
padding-left: 20px;
@ -1137,7 +1135,7 @@ export default class RegisterArea extends Vue {
outline: none;
}
.container input {
.checkmark-container input {
position: absolute;
opacity: 0;
cursor: pointer;
@ -1155,11 +1153,11 @@ export default class RegisterArea extends Vue {
border-radius: 4px;
}
.container:hover input ~ .checkmark {
.checkmark-container:hover input ~ .checkmark {
background-color: white;
}
.container input:checked ~ .checkmark {
.checkmark-container input:checked ~ .checkmark {
border: 2px solid #afb7c1;
background-color: transparent;
}
@ -1174,7 +1172,7 @@ export default class RegisterArea extends Vue {
border-color: red;
}
.container .checkmark:after {
.checkmark-container .checkmark:after {
left: 7px;
top: 3px;
width: 5px;
@ -1184,10 +1182,14 @@ export default class RegisterArea extends Vue {
transform: rotate(45deg);
}
.container input:checked ~ .checkmark:after {
.checkmark-container input:checked ~ .checkmark:after {
display: block;
}
::v-deep .grecaptcha-badge {
visibility: hidden;
}
@media screen and (max-width: 1429px) {
.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) {
.register-area {