Skip to content

Commit

Permalink
feat: display captcha component (no captcha, captcha V2, captcha V3) …
Browse files Browse the repository at this point in the history
…according to the related ICM captcha service
  • Loading branch information
Silke committed Apr 20, 2020
1 parent f85350a commit 81b03b1
Show file tree
Hide file tree
Showing 16 changed files with 101 additions and 75 deletions.
17 changes: 15 additions & 2 deletions src/app/core/facades/app.facade.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { combineLatest } from 'rxjs';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

import { getAvailableLocales, getCurrentLocale } from 'ish-core/store/configuration';
import { getAvailableLocales, getCurrentLocale, getServerConfigParameter } from 'ish-core/store/configuration';
import { LoadCountries, getAllCountries, getCountriesLoading } from 'ish-core/store/countries';
import { getGeneralError, getGeneralErrorType } from 'ish-core/store/error';
import { LoadRegions, getRegionsByCountryCode } from 'ish-core/store/regions';
Expand Down Expand Up @@ -54,4 +54,17 @@ export class AppFacade {
this.store.dispatch(new LoadRegions({ countryCode }));
return this.store.pipe(select(getRegionsByCountryCode, { countryCode }));
}

/**
*
* @param feature feature for captcha according to the store config like 'register', 'contactUs', 'forgotPassword'
* @returns is captcha enabled for this feature (flag), Captcha V2 Sitekey, Captcha V3 SiteKey (if no site key is returned, the appropriate captcha service is not activated)
*/
getCaptchaConfig$(feature: string): Observable<[boolean, string, string]> {
return combineLatest([
this.store.pipe(select(getServerConfigParameter<boolean>('captcha.' + feature))),
this.store.pipe(select(getServerConfigParameter<string>('services.ReCaptchaV2ServiceDefinition.SiteKey'))),
this.store.pipe(select(getServerConfigParameter<string>('services.ReCaptchaV3ServiceDefinition.SiteKey'))),
]);
}
}
7 changes: 7 additions & 0 deletions src/app/core/models/captcha/captcha.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* The contact request to send, when a customer want to get in contact with the shop
*/
export interface Captcha {
captcha?: string;
captchaAction?: string;
}
6 changes: 3 additions & 3 deletions src/app/core/models/contact/contact.model.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Captcha } from 'ish-core/models/captcha/captcha.model';

/**
* The contact request to send, when a customer want to get in contact with the shop
*/
export interface Contact {
export interface Contact extends Captcha {
name: string;
type?: string;
email: string;
phone: string;
order?: string;
subject: string;
comment: string;
captcha?: string;
captchaAction?: string;
}
5 changes: 2 additions & 3 deletions src/app/core/models/customer/customer.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address } from 'ish-core/models/address/address.model';
import { Captcha } from 'ish-core/models/captcha/captcha.model';
import { Credentials } from 'ish-core/models/credentials/credentials.model';
import { User } from 'ish-core/models/user/user.model';

Expand Down Expand Up @@ -29,9 +30,7 @@ export interface CustomerUserType {
/**
* registration request data type
*/
export interface CustomerRegistrationType extends CustomerUserType {
export interface CustomerRegistrationType extends CustomerUserType, Captcha {
credentials: Credentials;
address: Address;
captchaResponse?: string;
captchaAction?: string;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export interface PasswordReminder {
import { Captcha } from 'ish-core/models/captcha/captcha.model';

export interface PasswordReminder extends Captcha {
email: string;
firstName: string;
lastName: string;
answer?: string;
captcha?: string;
captchaAction?: string;
}
4 changes: 2 additions & 2 deletions src/app/core/services/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ export class UserService {
};
}

if (body.captchaResponse) {
if (body.captcha) {
return this.apiService.post<void>('customers', newCustomer, {
headers: this.apiService.appendCaptchaHeaders(body.captchaResponse, body.captchaAction),
headers: this.apiService.appendCaptchaHeaders(body.captcha, body.captchaAction),
});
// without captcha
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ContactFormComponent implements OnChanges, OnInit {
subject: ['', Validators.required],
comment: ['', Validators.required],
captcha: [''],
captchaAction: ['contact_us'],
captchaAction: ['contactUs'],
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class RequestReminderFormComponent implements OnInit {
firstName: new FormControl('', Validators.required),
lastName: new FormControl('', Validators.required),
captcha: new FormControl(''),
captchaAction: new FormControl('forgot_password'),
captchaAction: new FormControl('forgotPassword'),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class RegistrationFormComponent implements OnInit, OnChanges {
birthday: [''],
termsAndConditions: [false, [Validators.required, Validators.pattern('true')]],
captcha: [''],
captchaAction: ['create_account'],
captchaAction: ['register'],
address: this.afs.getFactory('default').getGroup({ isBusinessAddress: this.businessCustomerRegistration }), // filled dynamically when country code changes
});

Expand Down Expand Up @@ -147,7 +147,7 @@ export class RegistrationFormComponent implements OnInit, OnChanges {
}

const registration: CustomerRegistrationType = { customer, user, credentials, address };
registration.captchaResponse = this.form.get('captcha').value;
registration.captcha = this.form.get('captcha').value;
registration.captchaAction = this.form.get('captchaAction').value;

this.create.emit(registration);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="row form-group">
<div *ngIf="captchaSiteKey" class="row form-group">
<div [ngClass]="cssClass">
<div [ngClass]="{ 'has-error': hasError }">
<re-captcha [siteKey]="captchaSiteKey" (resolved)="resolved($event)" class="captcha"></re-captcha>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('Captcha V2 Component', () => {
fixture = TestBed.createComponent(CaptchaV2Component);
component = fixture.componentInstance;
element = fixture.nativeElement;
component.captchaSiteKey = 'captchaV2SiteKey';
});

it('should be created', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

import { CAPTCHA_SITE_KEY } from 'ish-core/configurations/injection-keys';

/**
* The Captcha V2 Component
*
Expand All @@ -14,12 +12,11 @@ import { CAPTCHA_SITE_KEY } from 'ish-core/configurations/injection-keys';
changeDetection: ChangeDetectionStrategy.Default,
})
export class CaptchaV2Component {
@Input() captchaSiteKey: string;
@Input() parentForm: FormGroup;
@Input() controlName: string;
@Input() cssClass: string;

constructor(@Inject(CAPTCHA_SITE_KEY) public captchaSiteKey) {}

/* writes the captcha response token in the captcha farm field */
resolved(captchaResponse: string) {
this.parentForm.get(this.controlName).setValue(captchaResponse);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<ish-captcha-v2
*ishFeature="'captchaV2'"
*ngIf="captchaV2SiteKey"
[captchaSiteKey]="captchaV2SiteKey"
[parentForm]="form"
[controlName]="controlName"
[cssClass]="cssClass"
></ish-captcha-v2>
<ish-captcha-v3
*ishFeature="'captchaV3'"
*ngIf="captchaV3SiteKey"
[parentForm]="form"
[controlName]="controlName"
[actionControlName]="actionControlName"
Expand Down
17 changes: 10 additions & 7 deletions src/app/shared/forms/components/captcha/captcha.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormControl, FormGroup } from '@angular/forms';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { anyString, instance, mock, when } from 'ts-mockito';

import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { AppFacade } from 'ish-core/facades/app.facade';
import { configurationReducer } from 'ish-core/store/configuration/configuration.reducer';
import { ngrxTesting } from 'ish-core/utils/dev/ngrx-testing';
import { CaptchaV2Component } from 'ish-shared/forms/components/captcha-v2/captcha-v2.component';
Expand All @@ -16,17 +18,17 @@ describe('Captcha Component', () => {
let element: HTMLElement;

beforeEach(async(() => {
const appFacadeMock = mock(AppFacade);
when(appFacadeMock.getCaptchaConfig$(anyString())).thenReturn(of([true, 'V2token', undefined]));

TestBed.configureTestingModule({
declarations: [CaptchaComponent, MockComponent(CaptchaV2Component), MockComponent(CaptchaV3Component)],
imports: [
FeatureToggleModule,
ngrxTesting({
reducers: { configuration: configurationReducer },
config: {
initialState: { configuration: { features: ['captchaV2'] } },
},
}),
],
providers: [{ provide: AppFacade, useFactory: () => instance(appFacadeMock) }],
}).compileComponents();
}));

Expand All @@ -46,6 +48,7 @@ describe('Captcha Component', () => {
expect(() => fixture.detectChanges()).not.toThrow();
expect(element).toMatchInlineSnapshot(`
<ish-captcha-v2
ng-reflect-captcha-site-key="V2token"
ng-reflect-control-name="captcha"
ng-reflect-css-class="offset-md-4 col-md-8"
></ish-captcha-v2>
Expand All @@ -68,9 +71,9 @@ describe('Captcha Component', () => {
});

it('should throw an error if there is no control with actionControlName in the given form', () => {
component.actionControlName = 'xxx';
component.actionControlName = 'yyy';
expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot(
`"input parameter <controlName> with value 'xxx' does not exist in the given form for CaptchaComponent"`
`"input parameter <actionControlName> with value 'yyy' does not exist in the given form for CaptchaComponent"`
);
});
});
87 changes: 47 additions & 40 deletions src/app/shared/forms/components/captcha/captcha.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormGroup, Validators } from '@angular/forms';
import { filter, take } from 'rxjs/operators';

import { FeatureToggleService } from 'ish-core/feature-toggle.module';
import { AppFacade } from 'ish-core/facades/app.facade';

/**
* The Captcha Component
Expand Down Expand Up @@ -39,70 +40,76 @@ export class CaptchaComponent implements OnInit {
*/
@Input() cssClass = 'offset-md-4 col-md-8';

captchaV2Enabled = false;
captchaV3Enabled = false;
captchaV2SiteKey: string;
captchaV3SiteKey: string;

constructor(private featureToggle: FeatureToggleService) {}
constructor(private appFacade: AppFacade) {}

ngOnInit() {
this.initComponent();
}

initComponent() {
this.captchaV2Enabled = this.featureToggle.enabled('captchaV2');
this.captchaV3Enabled = this.featureToggle.enabled('captchaV3');

/* check, if there is max only one captcha service enabled */
if (this.captchaV2Enabled && this.captchaV3Enabled) {
throw new Error('it is not allowed to enable more than one captcha service');
if (!this.form) {
throw new Error('required input parameter <form> is missing for FormElementComponent');
}

/* check, if required input parameters are available */
if (this.isCaptchaEnabled()) {
if (!this.form) {
throw new Error('required input parameter <form> is missing for FormElementComponent');
}
if (!this.formControl) {
throw new Error(
`input parameter <controlName> with value '${this.controlName}' does not exist in the given form for CaptchaComponent`
);
}
if (!this.actionFormControl) {
throw new Error(
`input parameter <controlName> with value '${this.actionControlName}' does not exist in the given form for CaptchaComponent`
);
}

// set captcha field required
this.formControl.setValidators([Validators.required]);
if (!this.formControl) {
throw new Error(
`input parameter <controlName> with value '${this.controlName}' does not exist in the given form for CaptchaComponent`
);
}

/* for captchaV2 the action is not needed, if the action is missing the service will send captcha in v2 format */
if (this.captchaV2Enabled) {
this.actionFormControl.setValue(undefined);
if (!this.actionFormControl) {
throw new Error(
`input parameter <actionControlName> with value '${this.actionControlName}' does not exist in the given form for CaptchaComponent`
);
}

// for captcha V3 the action is required
if (this.captchaV3Enabled) {
this.actionFormControl.setValidators([Validators.required]);
}
this.appFacade
.getCaptchaConfig$(this.actionFormControl.value)
.pipe(
filter(([enabled, v2, v3]) => enabled !== undefined && (!!v2 || !!v3)),
take(1)
)
.subscribe(([enabled, v2SiteKey, v3SiteKey]) => {
this.captchaV2SiteKey = enabled ? v2SiteKey : undefined;
// prevent to enable v2 and v3 at the same time
this.captchaV3SiteKey = enabled && !v2SiteKey ? v3SiteKey : undefined;

/* check, if required input parameters are available */
if (this.isCaptchaEnabled()) {
// set captcha field required
if (this.isCaptchaEnabled()) {
this.formControl.setValidators([Validators.required]);
}

/* for captchaV2 the action is not needed, if the action is missing the service will send captcha in v2 format */
if (this.captchaV2SiteKey) {
this.actionFormControl.setValue(undefined);
}

// for captcha V3 the action is required
if (this.captchaV3SiteKey) {
this.actionFormControl.setValidators([Validators.required]);
}
}
});
}

/**
* get the captcha form control according to the controlName
*/
get formControl(): AbstractControl {
return this.form.get(this.controlName);
return this.form && this.form.get(this.controlName);
}

/**
* get the captcha action form control according to the controlName
*/
get actionFormControl(): AbstractControl {
return this.form.get(this.actionControlName);
return this.form && this.form.get(this.actionControlName);
}

isCaptchaEnabled(): boolean {
return this.captchaV2Enabled || this.captchaV3Enabled;
return !!this.captchaV2SiteKey || !!this.captchaV3SiteKey;
}
}
4 changes: 1 addition & 3 deletions src/environments/environment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export interface Environment {

/* FEATURE TOOGLES */
features: (
| 'captchaV2'
| 'captchaV3'
| 'compare'
| 'rating'
| 'recently'
Expand Down Expand Up @@ -53,7 +51,7 @@ export interface Environment {
Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
*/
// TODO: get captcha site key, when Configuration Response in REST API is available
// captcha site key for captcha V3
captchaSiteKey?: string;

/* PROGRESSIVE WEB APP CONFIGURATIONS */
Expand Down

0 comments on commit 81b03b1

Please sign in to comment.