Skip to content

Commit

Permalink
fix: set captcha authorization key for 'contact us' REST requests (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
SGrueber authored and dhhyi committed May 24, 2020
1 parent 663a361 commit a119c88
Show file tree
Hide file tree
Showing 16 changed files with 101 additions and 64 deletions.
2 changes: 1 addition & 1 deletion e2e/cypress/integration/pages/contact/contact.page.ts
Expand Up @@ -28,7 +28,7 @@ export class ContactPage {
this.phoneInput.clear().type(phone).blur();
// tslint:disable-next-line:ban
cy.get('select[data-testing-id="subject"]').select(subject);
cy.get('textarea[data-testing-id="comments"]').clear().type(comments).blur();
cy.get('textarea[data-testing-id="comment"]').clear().type(comments).blur();
return this;
}

Expand Down
7 changes: 7 additions & 0 deletions src/app/core/models/captcha/captcha.model.ts
@@ -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;
}
4 changes: 3 additions & 1 deletion src/app/core/models/contact/contact.model.ts
@@ -1,7 +1,9 @@
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;
Expand Down
5 changes: 2 additions & 3 deletions src/app/core/models/customer/customer.model.ts
@@ -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;
}
@@ -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;
}
27 changes: 27 additions & 0 deletions src/app/core/services/api/api.service.spec.ts
Expand Up @@ -487,5 +487,32 @@ describe('Api Service', () => {
const req = httpTestingController.expectOne(`${REST_URL}/dummy`);
expect(req.request.headers.has(ApiService.TOKEN_HEADER_KEY)).toBeFalse();
});

it('should set Captcha V2 authorization header key when captcha is supplied without captchaAction', () => {
apiService.get('dummy', { captcha: { captcha: 'captchatoken' } }).subscribe(fail, fail, fail);

const req = httpTestingController.expectOne(`${REST_URL}/dummy`);
expect(req.request.headers.get(ApiService.AUTHORIZATION_HEADER_KEY)).toMatchInlineSnapshot(
`"CAPTCHA g-recaptcha-response=captchatoken foo=bar"`
);
});

it('should set Captcha V3 authorization header key when captcha is supplied', () => {
apiService
.get('dummy', { captcha: { captcha: 'captchatoken', captchaAction: 'create_account' } })
.subscribe(fail, fail, fail);

const req = httpTestingController.expectOne(`${REST_URL}/dummy`);
expect(req.request.headers.get(ApiService.AUTHORIZATION_HEADER_KEY)).toMatchInlineSnapshot(
`"CAPTCHA recaptcha_token=captchatoken action=create_account"`
);
});

it('should not set header when captcha config object is empty', () => {
apiService.get('dummy', { captcha: {} }).subscribe(fail, fail, fail);

const req = httpTestingController.expectOne(`${REST_URL}/dummy`);
expect(req.request.headers.get(ApiService.AUTHORIZATION_HEADER_KEY)).toBeFalsy();
});
});
});
33 changes: 31 additions & 2 deletions src/app/core/services/api/api.service.ts
Expand Up @@ -4,6 +4,7 @@ import { Store, select } from '@ngrx/store';
import { Observable, OperatorFunction, Subject, forkJoin, of, throwError } from 'rxjs';
import { catchError, concatMap, defaultIfEmpty, filter, map, switchMap, tap, throwIfEmpty } from 'rxjs/operators';

import { Captcha } from 'ish-core/models/captcha/captcha.model';
import { Link } from 'ish-core/models/link/link.model';
import { Locale } from 'ish-core/models/locale/locale.model';
import { getCurrentLocale, getICMServerURL, getRestEndpoint } from 'ish-core/store/configuration';
Expand Down Expand Up @@ -105,6 +106,7 @@ export interface AvailableOptions {
headers?: HttpHeaders;
skipApiErrorHandling?: boolean;
runExclusively?: boolean;
captcha?: Captcha;
}

@Injectable({ providedIn: 'root' })
Expand Down Expand Up @@ -137,17 +139,44 @@ export class ApiService {
: headers;
}

/**
- * sets the request header for the appropriate captcha service
- @param captcha captcha token for captcha V2 and V3
- @param captchaAction captcha action for captcha V3
- @returns HttpHeader http header with captcha Authorization key
- */
private appendCaptchaTokenToHeaders(headers: HttpHeaders, captcha: string, captchaAction: string) {
// testing token gets 'null' from captcha service, so we accept it as a valid value here
if (captchaAction !== undefined) {
// captcha V3
return headers.set(
ApiService.AUTHORIZATION_HEADER_KEY,
`CAPTCHA recaptcha_token=${captcha} action=${captchaAction}`
);
} else {
// captcha V2
// TODO: remove second parameter 'foo=bar' that currently only resolves a shortcoming of the server side implemenation that still requires two parameters
return headers.set(ApiService.AUTHORIZATION_HEADER_KEY, `CAPTCHA g-recaptcha-response=${captcha} foo=bar`);
}
}

/**
* merges supplied and default headers
*/
private constructHeaders(options?: { headers?: HttpHeaders }): HttpHeaders {
private constructHeaders(options?: AvailableOptions): HttpHeaders {
const defaultHeaders = new HttpHeaders().set('content-type', 'application/json').set('Accept', 'application/json');

let newHeaders = defaultHeaders;
if (options && options.headers) {
newHeaders = options.headers.keys().reduce((acc, key) => acc.set(key, options.headers.get(key)), defaultHeaders);
}
return this.appendAPITokenToHeaders(newHeaders);

// testing token gets 'null' from captcha service, so we accept it as a valid value here
if (options && options.captcha && options.captcha.captcha !== undefined) {
return this.appendCaptchaTokenToHeaders(newHeaders, options.captcha.captcha, options.captcha.captchaAction);
} else {
return this.appendAPITokenToHeaders(newHeaders);
}
}

private wrapHttpCall<T>(httpCall: () => Observable<T>, options: AvailableOptions) {
Expand Down
10 changes: 8 additions & 2 deletions src/app/core/services/contact/contact.service.ts
@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { pick } from 'lodash-es';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { Contact } from 'ish-core/models/contact/contact.model';
import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';
import { ApiService, AvailableOptions, unpackEnvelope } from 'ish-core/services/api/api.service';

@Injectable({ providedIn: 'root' })
export class ContactService {
Expand All @@ -23,6 +24,11 @@ export class ContactService {
* Send contact us request, when a customer want to get in contact with the shop
*/
createContactRequest(contactData: Contact): Observable<void> {
return this.apiService.post(`contact`, contactData, { skipApiErrorHandling: true });
const options: AvailableOptions = {
skipApiErrorHandling: true,
captcha: pick(contactData, ['captcha', 'captchaAction']),
};

return this.apiService.post(`contact`, { ...contactData, captcha: undefined, captchaAction: undefined }, options);
}
}
4 changes: 2 additions & 2 deletions src/app/core/services/user/user.service.spec.ts
Expand Up @@ -80,7 +80,7 @@ describe('User Service', () => {
});

it("should create a new private user when 'createUser' is called with type 'PrivateCustomer'", done => {
when(apiServiceMock.post(anyString(), anything())).thenReturn(of({}));
when(apiServiceMock.post(anyString(), anything(), anything())).thenReturn(of({}));

const payload = {
customer: { customerNo: '4711', type: 'PrivateCustomer' } as Customer,
Expand All @@ -90,7 +90,7 @@ describe('User Service', () => {
} as CustomerRegistrationType;

userService.createUser(payload).subscribe(() => {
verify(apiServiceMock.post('customers', anything())).once();
verify(apiServiceMock.post('customers', anything(), anything())).once();
done();
});
});
Expand Down
34 changes: 5 additions & 29 deletions src/app/core/services/user/user.service.ts
@@ -1,6 +1,7 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import b64u from 'b64u';
import { pick } from 'lodash-es';
import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

Expand Down Expand Up @@ -97,14 +98,9 @@ export class UserService {
};
}

if (body.captchaResponse) {
return this.apiService.post<void>('customers', newCustomer, {
headers: this.appendCaptchaHeaders(body.captchaResponse, body.captchaAction),
});
// without captcha
} else {
return this.apiService.post<void>('customers', newCustomer);
}
return this.apiService.post('customers', newCustomer, {
captcha: pick(body, ['captcha', 'captchaAction']),
});
}

/**
Expand Down Expand Up @@ -197,12 +193,9 @@ export class UserService {
requestPasswordReminder(data: PasswordReminder) {
const options: AvailableOptions = {
skipApiErrorHandling: true,
captcha: pick(data, ['captcha', 'captchaAction']),
};

if (data.captcha) {
options.headers = this.appendCaptchaHeaders(data.captcha, data.captchaAction);
}

return this.apiService.post('security/reminder', { answer: '', ...data }, options);
}

Expand All @@ -216,21 +209,4 @@ export class UserService {
};
return this.apiService.post('security/password', data, options);
}

// provides the request header for the appropriate captcha service
private appendCaptchaHeaders(captcha: string, captchaAction: string): HttpHeaders {
let headers = new HttpHeaders();
// captcha V3
if (captchaAction) {
headers = headers.set(
ApiService.AUTHORIZATION_HEADER_KEY,
`CAPTCHA recaptcha_token=${captcha} action=${captchaAction}`
);
// captcha V2
} else {
// TODO: remove second parameter 'foo=bar' that currently only resolves a shortcoming of the server side implemenation that still requires two parameters
headers = headers.set(ApiService.AUTHORIZATION_HEADER_KEY, `CAPTCHA g-recaptcha-response=${captcha} foo=bar`);
}
return headers;
}
}
Expand Up @@ -29,7 +29,7 @@
[translateOptionLabels]="true"
></ish-select>
<ish-textarea
controlName="comments"
controlName="comment"
[errorMessages]="{ required: 'helpdesk.contactus.comments.error' }"
[form]="contactForm"
label="helpdesk.contactus.comments.label"
Expand Down
Expand Up @@ -69,7 +69,7 @@ describe('Contact Form Component', () => {
component.contactForm.get('phone').setValue('123456');
component.contactForm.get('order').setValue('456789');
component.contactForm.get('subject').setValue('Return');
component.contactForm.get('comments').setValue('want to return stuff');
component.contactForm.get('comment').setValue('want to return stuff');
component.submitForm();
verify(emitter.emit(anything())).once();
});
Expand Down
19 changes: 5 additions & 14 deletions src/app/pages/contact/contact-form/contact-form.component.ts
Expand Up @@ -22,7 +22,7 @@ export class ContactFormComponent implements OnChanges, OnInit {
@Input() subjects: string[] = [];
@Input() user: User;
/** The contact request to send. */
@Output() request = new EventEmitter<{ contact: Contact; captcha?: string }>();
@Output() request = new EventEmitter<Contact>();

subjectOptions: SelectOption[];

Expand All @@ -43,18 +43,9 @@ export class ContactFormComponent implements OnChanges, OnInit {
/** emit contact request, when for is valid or mark form as dirty, when form is invalid */
submitForm() {
if (this.contactForm.valid) {
const formValue = this.contactForm.value;
const contact: Contact = {
name: formValue.name,
email: formValue.email,
phone: formValue.phone,
subject: formValue.subject,
comment: formValue.comments,
order: formValue.order,
};
const contact: Contact = this.contactForm.value;

/* ToDo: send captcha data if captcha is supported by REST, see #IS-28299 */
this.request.emit({ contact });
this.request.emit(contact);
} else {
markAsDirtyRecursive(this.contactForm);
this.submitted = true;
Expand All @@ -77,9 +68,9 @@ export class ContactFormComponent implements OnChanges, OnInit {
phone: [phone, Validators.required],
order: [''],
subject: ['', Validators.required],
comments: ['', Validators.required],
comment: ['', Validators.required],
captcha: [''],
captchaAction: ['contact_us'],
captchaAction: ['contactUs'],
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/contact/contact-page.component.ts
Expand Up @@ -52,8 +52,8 @@ export class ContactPageComponent implements OnInit, OnDestroy {
}

/** dispatch contact request */
createRequest(request: { contact: Contact; captcha?: string }) {
this.accountFacade.createContact(request.contact);
createRequest(contact: Contact) {
this.accountFacade.createContact(contact);
this.router.navigate([], { queryParams: { submitted: true } });
}

Expand Down
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
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

0 comments on commit a119c88

Please sign in to comment.