mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
Feature/use hcaptcha on register if bot (#434)
* Parse captcha required from error messages CaptchaProtectedAttribute produces an error with captcha information. We want to parse that data out to make it easily accessible to components * Don't show error on catpcha The component should hande this situation. * Add captchaResponse to captcha protected api endpoints * Extract captcha logic to abstract base class * Add captcha to register * linter fixes * Make sure to log Captcha required responses * Match file naming convention * Separate import into logical groups by folder * PR review
This commit is contained in:
50
angular/src/components/captchaProtected.component.ts
Normal file
50
angular/src/components/captchaProtected.component.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Directive, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
|
||||||
|
import { CaptchaIFrame } from 'jslib-common/misc/captcha_iframe';
|
||||||
|
|
||||||
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export abstract class CaptchaProtectedComponent {
|
||||||
|
@Input() captchaSiteKey: string = null;
|
||||||
|
captchaToken: string = null;
|
||||||
|
captcha: CaptchaIFrame;
|
||||||
|
|
||||||
|
constructor(protected environmentService: EnvironmentService, protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService) { }
|
||||||
|
|
||||||
|
async setupCaptcha() {
|
||||||
|
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
if (webVaultUrl == null) {
|
||||||
|
webVaultUrl = 'https://vault.bitwarden.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.captcha = new CaptchaIFrame(window, webVaultUrl,
|
||||||
|
this.i18nService, (token: string) => {
|
||||||
|
this.captchaToken = token;
|
||||||
|
}, (error: string) => {
|
||||||
|
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
|
||||||
|
}, (info: string) => {
|
||||||
|
this.platformUtilsService.showToast('info', this.i18nService.t('info'), info);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showCaptcha() {
|
||||||
|
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleCaptchaRequired(response: { captchaSiteKey: string; }): boolean {
|
||||||
|
if (Utils.isNullOrWhitespace(response.captchaSiteKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.captchaSiteKey = response.captchaSiteKey;
|
||||||
|
this.captcha.init(response.captchaSiteKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,24 +19,22 @@ import { StorageService } from 'jslib-common/abstractions/storage.service';
|
|||||||
|
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
import { ConstantsService } from 'jslib-common/services/constants.service';
|
||||||
|
|
||||||
import { CaptchaIFrame } from 'jslib-common/misc/captcha_iframe';
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from './captchaProtected.component';
|
||||||
|
|
||||||
const Keys = {
|
const Keys = {
|
||||||
rememberedEmail: 'rememberedEmail',
|
rememberedEmail: 'rememberedEmail',
|
||||||
rememberEmail: 'rememberEmail',
|
rememberEmail: 'rememberEmail',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
@Input() email: string = '';
|
@Input() email: string = '';
|
||||||
@Input() rememberEmail = true;
|
@Input() rememberEmail = true;
|
||||||
|
|
||||||
masterPassword: string = '';
|
masterPassword: string = '';
|
||||||
showPassword: boolean = false;
|
showPassword: boolean = false;
|
||||||
captchaSiteKey: string = null;
|
|
||||||
captchaToken: string = null;
|
|
||||||
captcha: CaptchaIFrame;
|
|
||||||
formPromise: Promise<AuthResult>;
|
formPromise: Promise<AuthResult>;
|
||||||
onSuccessfulLogin: () => Promise<any>;
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
@@ -46,10 +44,12 @@ export class LoginComponent implements OnInit {
|
|||||||
protected successRoute = 'vault';
|
protected successRoute = 'vault';
|
||||||
|
|
||||||
constructor(protected authService: AuthService, protected router: Router,
|
constructor(protected authService: AuthService, protected router: Router,
|
||||||
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
|
platformUtilsService: PlatformUtilsService, i18nService: I18nService,
|
||||||
protected stateService: StateService, protected environmentService: EnvironmentService,
|
protected stateService: StateService, environmentService: EnvironmentService,
|
||||||
protected passwordGenerationService: PasswordGenerationService,
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService) { }
|
protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (this.email == null || this.email === '') {
|
if (this.email == null || this.email === '') {
|
||||||
@@ -66,19 +66,7 @@ export class LoginComponent implements OnInit {
|
|||||||
this.focusInput();
|
this.focusInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
this.setupCaptcha();
|
||||||
if (webVaultUrl == null) {
|
|
||||||
webVaultUrl = 'https://vault.bitwarden.com';
|
|
||||||
}
|
|
||||||
this.captcha = new CaptchaIFrame(window, webVaultUrl,
|
|
||||||
this.i18nService, (token: string) => {
|
|
||||||
this.captchaToken = token;
|
|
||||||
}, (error: string) => {
|
|
||||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
|
|
||||||
}, (info: string) => {
|
|
||||||
this.platformUtilsService.showToast('info', this.i18nService.t('info'), info);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@@ -107,9 +95,8 @@ export class LoginComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
await this.storageService.remove(Keys.rememberedEmail);
|
await this.storageService.remove(Keys.rememberedEmail);
|
||||||
}
|
}
|
||||||
if (!Utils.isNullOrWhitespace(response.captchaSiteKey)) {
|
if (this.handleCaptchaRequired(response)) {
|
||||||
this.captchaSiteKey = response.captchaSiteKey;
|
return;
|
||||||
this.captcha.init(response.captchaSiteKey);
|
|
||||||
} else if (response.twoFactor) {
|
} else if (response.twoFactor) {
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
this.onSuccessfulLoginTwoFactorNavigate();
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
@@ -165,9 +152,6 @@ export class LoginComponent implements OnInit {
|
|||||||
'&state=' + state + '&codeChallenge=' + codeChallenge);
|
'&state=' + state + '&codeChallenge=' + codeChallenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
showCaptcha() {
|
|
||||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
|
||||||
}
|
|
||||||
protected focusInput() {
|
protected focusInput() {
|
||||||
document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus();
|
document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Directive, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { KeysRequest } from 'jslib-common/models/request/keysRequest';
|
import { KeysRequest } from 'jslib-common/models/request/keysRequest';
|
||||||
@@ -7,6 +8,7 @@ import { RegisterRequest } from 'jslib-common/models/request/registerRequest';
|
|||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
|
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
|
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
@@ -14,7 +16,10 @@ import { StateService } from 'jslib-common/abstractions/state.service';
|
|||||||
|
|
||||||
import { KdfType } from 'jslib-common/enums/kdfType';
|
import { KdfType } from 'jslib-common/enums/kdfType';
|
||||||
|
|
||||||
export class RegisterComponent {
|
import { CaptchaProtectedComponent } from './captchaProtected.component';
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class RegisterComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
name: string = '';
|
name: string = '';
|
||||||
email: string = '';
|
email: string = '';
|
||||||
masterPassword: string = '';
|
masterPassword: string = '';
|
||||||
@@ -31,13 +36,18 @@ export class RegisterComponent {
|
|||||||
private masterPasswordStrengthTimeout: any;
|
private masterPasswordStrengthTimeout: any;
|
||||||
|
|
||||||
constructor(protected authService: AuthService, protected router: Router,
|
constructor(protected authService: AuthService, protected router: Router,
|
||||||
protected i18nService: I18nService, protected cryptoService: CryptoService,
|
i18nService: I18nService, protected cryptoService: CryptoService,
|
||||||
protected apiService: ApiService, protected stateService: StateService,
|
protected apiService: ApiService, protected stateService: StateService,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
protected passwordGenerationService: PasswordGenerationService) {
|
protected passwordGenerationService: PasswordGenerationService, environmentService: EnvironmentService) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
this.showTerms = !platformUtilsService.isSelfHost();
|
this.showTerms = !platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.setupCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
get masterPasswordScoreWidth() {
|
get masterPasswordScoreWidth() {
|
||||||
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
||||||
}
|
}
|
||||||
@@ -127,7 +137,7 @@ export class RegisterComponent {
|
|||||||
const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key);
|
const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||||
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||||
const request = new RegisterRequest(this.email, this.name, hashedPassword,
|
const request = new RegisterRequest(this.email, this.name, hashedPassword,
|
||||||
this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData);
|
this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData, this.captchaToken);
|
||||||
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
||||||
const orgInvite = await this.stateService.get<any>('orgInvitation');
|
const orgInvite = await this.stateService.get<any>('orgInvitation');
|
||||||
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
|
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
|
||||||
@@ -137,7 +147,15 @@ export class RegisterComponent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.apiService.postRegister(request);
|
this.formPromise = this.apiService.postRegister(request);
|
||||||
await this.formPromise;
|
try {
|
||||||
|
await this.formPromise;
|
||||||
|
} catch (e) {
|
||||||
|
if (this.handleCaptchaRequired(e)) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated'));
|
this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated'));
|
||||||
this.router.navigate([this.successRoute], { queryParams: { email: this.email } });
|
this.router.navigate([this.successRoute], { queryParams: { email: this.email } });
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||||
|
|
||||||
|
import { ErrorResponse } from 'jslib-common/models/response';
|
||||||
|
|
||||||
import { ValidationService } from '../services/validation.service';
|
import { ValidationService } from '../services/validation.service';
|
||||||
|
|
||||||
@@ -13,7 +16,8 @@ import { ValidationService } from '../services/validation.service';
|
|||||||
export class ApiActionDirective implements OnChanges {
|
export class ApiActionDirective implements OnChanges {
|
||||||
@Input() appApiAction: Promise<any>;
|
@Input() appApiAction: Promise<any>;
|
||||||
|
|
||||||
constructor(private el: ElementRef, private validationService: ValidationService) { }
|
constructor(private el: ElementRef, private validationService: ValidationService,
|
||||||
|
private logService: LogService) { }
|
||||||
|
|
||||||
ngOnChanges(changes: any) {
|
ngOnChanges(changes: any) {
|
||||||
if (this.appApiAction == null || this.appApiAction.then == null) {
|
if (this.appApiAction == null || this.appApiAction.then == null) {
|
||||||
@@ -26,6 +30,11 @@ export class ApiActionDirective implements OnChanges {
|
|||||||
this.el.nativeElement.loading = false;
|
this.el.nativeElement.loading = false;
|
||||||
}, (e: any) => {
|
}, (e: any) => {
|
||||||
this.el.nativeElement.loading = false;
|
this.el.nativeElement.loading = false;
|
||||||
|
|
||||||
|
if (e instanceof ErrorResponse && (e as ErrorResponse).captchaRequired) {
|
||||||
|
this.logService.error('Captcha required error response: ' + e.getSingleMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
3
common/src/models/request/captchaProtectedRequest.ts
Normal file
3
common/src/models/request/captchaProtectedRequest.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export abstract class CaptchaProtectedRequest {
|
||||||
|
captchaResponse: string = null;
|
||||||
|
}
|
||||||
@@ -3,28 +3,18 @@ import { ReferenceEventRequest } from './referenceEventRequest';
|
|||||||
|
|
||||||
import { KdfType } from '../../enums/kdfType';
|
import { KdfType } from '../../enums/kdfType';
|
||||||
|
|
||||||
export class RegisterRequest {
|
import { CaptchaProtectedRequest } from './captchaProtectedRequest';
|
||||||
name: string;
|
|
||||||
email: string;
|
export class RegisterRequest implements CaptchaProtectedRequest {
|
||||||
masterPasswordHash: string;
|
|
||||||
masterPasswordHint: string;
|
masterPasswordHint: string;
|
||||||
key: string;
|
|
||||||
keys: KeysRequest;
|
keys: KeysRequest;
|
||||||
token: string;
|
token: string;
|
||||||
organizationUserId: string;
|
organizationUserId: string;
|
||||||
kdf: KdfType;
|
|
||||||
kdfIterations: number;
|
|
||||||
referenceData: ReferenceEventRequest;
|
|
||||||
|
|
||||||
constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string,
|
|
||||||
kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventRequest) {
|
constructor(public email: string, public name: string, public masterPasswordHash: string,
|
||||||
this.name = name;
|
masterPasswordHint: string, public key: string, public kdf: KdfType, public kdfIterations: number,
|
||||||
this.email = email;
|
public referenceData: ReferenceEventRequest, public captchaResponse: string) {
|
||||||
this.masterPasswordHash = masterPasswordHash;
|
|
||||||
this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null;
|
this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null;
|
||||||
this.key = key;
|
|
||||||
this.kdf = kdf;
|
|
||||||
this.kdfIterations = kdfIterations;
|
|
||||||
this.referenceData = referenceData;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { TwoFactorProviderType } from '../../enums/twoFactorProviderType';
|
import { TwoFactorProviderType } from '../../enums/twoFactorProviderType';
|
||||||
|
|
||||||
|
import { CaptchaProtectedRequest } from './captchaProtectedRequest';
|
||||||
import { DeviceRequest } from './deviceRequest';
|
import { DeviceRequest } from './deviceRequest';
|
||||||
|
|
||||||
export class TokenRequest {
|
export class TokenRequest implements CaptchaProtectedRequest {
|
||||||
email: string;
|
email: string;
|
||||||
masterPasswordHash: string;
|
masterPasswordHash: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -10,14 +11,10 @@ export class TokenRequest {
|
|||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
token: string;
|
|
||||||
provider: TwoFactorProviderType;
|
|
||||||
remember: boolean;
|
|
||||||
captchaToken: string;
|
|
||||||
device?: DeviceRequest;
|
device?: DeviceRequest;
|
||||||
|
|
||||||
constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], provider: TwoFactorProviderType,
|
constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], public provider: TwoFactorProviderType,
|
||||||
token: string, remember: boolean, captchaToken: string, device?: DeviceRequest) {
|
public token: string, public remember: boolean, public captchaResponse: string, device?: DeviceRequest) {
|
||||||
if (credentials != null && credentials.length > 1) {
|
if (credentials != null && credentials.length > 1) {
|
||||||
this.email = credentials[0];
|
this.email = credentials[0];
|
||||||
this.masterPasswordHash = credentials[1];
|
this.masterPasswordHash = credentials[1];
|
||||||
@@ -29,11 +26,7 @@ export class TokenRequest {
|
|||||||
this.clientId = clientIdClientSecret[0];
|
this.clientId = clientIdClientSecret[0];
|
||||||
this.clientSecret = clientIdClientSecret[1];
|
this.clientSecret = clientIdClientSecret[1];
|
||||||
}
|
}
|
||||||
this.token = token;
|
|
||||||
this.provider = provider;
|
|
||||||
this.remember = remember;
|
|
||||||
this.device = device != null ? device : null;
|
this.device = device != null ? device : null;
|
||||||
this.captchaToken = captchaToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toIdentityToken(clientId: string) {
|
toIdentityToken(clientId: string) {
|
||||||
@@ -73,8 +66,8 @@ export class TokenRequest {
|
|||||||
obj.twoFactorRemember = this.remember ? '1' : '0';
|
obj.twoFactorRemember = this.remember ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.captchaToken != null) {
|
if (this.captchaResponse != null) {
|
||||||
obj.captchaResponse = this.captchaToken;
|
obj.captchaResponse = this.captchaResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { Utils } from '../../misc/utils';
|
||||||
|
|
||||||
import { BaseResponse } from './baseResponse';
|
import { BaseResponse } from './baseResponse';
|
||||||
|
|
||||||
export class ErrorResponse extends BaseResponse {
|
export class ErrorResponse extends BaseResponse {
|
||||||
message: string;
|
message: string;
|
||||||
validationErrors: { [key: string]: string[]; };
|
validationErrors: { [key: string]: string[]; };
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
|
captchaRequired: boolean;
|
||||||
|
captchaSiteKey: string;
|
||||||
|
|
||||||
constructor(response: any, status: number, identityResponse?: boolean) {
|
constructor(response: any, status: number, identityResponse?: boolean) {
|
||||||
super(response);
|
super(response);
|
||||||
@@ -20,6 +24,8 @@ export class ErrorResponse extends BaseResponse {
|
|||||||
if (errorModel) {
|
if (errorModel) {
|
||||||
this.message = this.getResponseProperty('Message', errorModel);
|
this.message = this.getResponseProperty('Message', errorModel);
|
||||||
this.validationErrors = this.getResponseProperty('ValidationErrors', errorModel);
|
this.validationErrors = this.getResponseProperty('ValidationErrors', errorModel);
|
||||||
|
this.captchaSiteKey = this.validationErrors?.HCaptcha_SiteKey?.[0];
|
||||||
|
this.captchaRequired = !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||||
} else {
|
} else {
|
||||||
if (status === 429) {
|
if (status === 429) {
|
||||||
this.message = 'Rate limit exceeded. Try again later.';
|
this.message = 'Rate limit exceeded. Try again later.';
|
||||||
|
|||||||
Reference in New Issue
Block a user