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_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
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -128,7 +128,8 @@ type Service struct {
|
||||
partners *rewards.PartnersService
|
||||
accounts payments.Accounts
|
||||
depositWallets payments.DepositWallets
|
||||
captchaHandler CaptchaHandler
|
||||
registrationCaptchaHandler CaptchaHandler
|
||||
loginCaptchaHandler CaptchaHandler
|
||||
analytics *analytics.Service
|
||||
tokens *consoleauth.Service
|
||||
|
||||
@ -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,11 +197,21 @@ 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{
|
||||
@ -210,7 +225,8 @@ func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting
|
||||
partners: partners,
|
||||
accounts: accounts,
|
||||
depositWallets: depositWallets,
|
||||
captchaHandler: captchaHandler,
|
||||
registrationCaptchaHandler: registrationCaptchaHandler,
|
||||
loginCaptchaHandler: loginCaptchaHandler,
|
||||
analytics: analytics,
|
||||
tokens: tokens,
|
||||
config: config,
|
||||
@ -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 {
|
||||
|
@ -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:"-"`
|
||||
}
|
||||
|
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
|
||||
# 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
|
||||
|
||||
|
@ -23,10 +23,14 @@
|
||||
<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="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 }}">
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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,33 +1049,15 @@ 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;
|
||||
|
||||
&__label-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
&__error {
|
||||
font-size: 16px;
|
||||
@ -1071,7 +1066,6 @@ export default class RegisterArea extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user