Skip to content

Commit

Permalink
[#11878] Add CAPTCHA to ARF (#13081)
Browse files Browse the repository at this point in the history
* Add captcha to ARF

* Update front-end tests

* Fix lint errors

* Change captcha to uppercase in error text

* Return captcha response when the getter is called

---------

Co-authored-by: Jay Aljelo Ting <65202977+jayasting98@users.noreply.github.com>
  • Loading branch information
xenosf and jayasting98 authored Apr 23, 2024
1 parent 76db4cc commit 6b83e4f
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 2 deletions.
10 changes: 10 additions & 0 deletions src/main/java/teammates/ui/request/AccountCreateRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class AccountCreateRequest extends BasicRequest {
private String instructorInstitution;
@Nullable
private String instructorComments;
@Nullable
private String captchaResponse;

public String getInstructorEmail() {
return instructorEmail;
Expand All @@ -35,6 +37,10 @@ public String getInstructorComments() {
return this.instructorComments;
}

public String getCaptchaResponse() {
return this.captchaResponse;
}

public void setInstructorName(String name) {
this.instructorName = name;
}
Expand All @@ -51,6 +57,10 @@ public void setInstructorComments(String instructorComments) {
this.instructorComments = instructorComments;
}

public void setCaptchaResponse(String captchaResponse) {
this.captchaResponse = captchaResponse;
}

@Override
public void validate() throws InvalidHttpRequestBodyException {
assertTrue(this.instructorEmail != null, "email cannot be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public JsonResult execute()
throws InvalidHttpRequestBodyException, InvalidOperationException {
AccountCreateRequest createRequest = getAndValidateRequestBody(AccountCreateRequest.class);

if (userInfo == null || !userInfo.isAdmin) {
String userCaptchaResponse = createRequest.getCaptchaResponse();
if (!recaptchaVerifier.isVerificationSuccessful(userCaptchaResponse)) {
throw new InvalidHttpRequestBodyException("Something went wrong with "
+ "the reCAPTCHA verification. Please try again.");
}
}

String instructorName = createRequest.getInstructorName().trim();
String instructorEmail = createRequest.getInstructorEmail().trim();
String instructorInstitution = createRequest.getInstructorInstitution().trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ exports[`InstructorRequestFormComponent should render correctly 1`] = `
STUDENT_NAME_MAX_LENGTH={[Function Number]}
accountService={[Function Object]}
arf={[Function FormGroup]}
captchaSiteKey=""
comments={[Function FormControl2]}
country={[Function FormControl2]}
email={[Function FormControl2]}
hasSubmitAttempt="false"
institution={[Function FormControl2]}
isCaptchaSuccessful="false"
isLoading="false"
lang={[Function String]}
name={[Function FormControl2]}
requestSubmissionEvent={[Function EventEmitter_]}
serverErrorMessage=""
size={[Function String]}
>
<p
aria-hidden="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@
[attr.aria-invalid]="comments.invalid"></textarea>
</div>
<br>
<div *ngIf="captchaSiteKey !== ''" class="form-group">
<ngx-recaptcha2 #captchaElem
[siteKey]="captchaSiteKey"
(success)="handleCaptchaSuccess($event)"
[useGlobalDomain]="false"
[size]="size"
[hl]="lang"
formControlName="recaptcha"
class="{{!isCaptchaSuccessful ? ' is-invalid' : ''}}">
</ngx-recaptcha2>
<div *ngIf="!isCaptchaSuccessful && hasSubmitAttempt" role="alert" tabindex="0"
class="invalid-feedback">
Please complete the CAPTCHA verification.
</div>
<br>
</div>
<ngb-alert type="danger" [dismissible]="false" *ngIf="hasSubmitAttempt && arf.invalid" class="error-box">
<strong>There was a problem with your submission.</strong> Please check and fix the errors above and submit again.
</ngb-alert>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NgxCaptchaModule } from 'ngx-captcha';
import { Observable, first } from 'rxjs';
import { InstructorRequestFormModel } from './instructor-request-form-model';
import { InstructorRequestFormComponent } from './instructor-request-form.component';
Expand Down Expand Up @@ -46,7 +47,7 @@ describe('InstructorRequestFormComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [InstructorRequestFormComponent],
imports: [ReactiveFormsModule],
imports: [ReactiveFormsModule, NgxCaptchaModule],
providers: [{ provide: AccountService, useValue: accountServiceStub }],
})
.compileComponents();
Expand All @@ -56,10 +57,15 @@ describe('InstructorRequestFormComponent', () => {
fixture = TestBed.createComponent(InstructorRequestFormComponent);
component = fixture.componentInstance;
accountService = TestBed.inject(AccountService);
component.captchaSiteKey = ''; // Test ignores captcha
fixture.detectChanges();
jest.clearAllMocks();
});

it('should have empty captcha key', () => {
expect(component).toBeTruthy();
});

it('should create', () => {
expect(component).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, EventEmitter, Output } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { finalize } from 'rxjs';
import { InstructorRequestFormModel } from './instructor-request-form-model';
import { environment } from '../../../../environments/environment';
import { AccountService } from '../../../../services/account.service';
import { AccountCreateRequest } from '../../../../types/api-request';
import { FormValidator } from '../../../../types/form-validator';
Expand All @@ -22,6 +23,13 @@ export class InstructorRequestFormComponent {
readonly COUNTRY_NAME_MAX_LENGTH = FormValidator.COUNTRY_NAME_MAX_LENGTH;
readonly EMAIL_MAX_LENGTH = FormValidator.EMAIL_MAX_LENGTH;

// Captcha
captchaSiteKey: string = environment.captchaSiteKey;
isCaptchaSuccessful: boolean = false;
captchaResponse?: string;
size: 'compact' | 'normal' = 'normal';
lang: string = 'en';

arf = new FormGroup({
name: new FormControl('', [
Validators.required,
Expand All @@ -44,6 +52,7 @@ export class InstructorRequestFormComponent {
Validators.maxLength(FormValidator.EMAIL_MAX_LENGTH),
]),
comments: new FormControl(''),
recaptcha: new FormControl(''),
}, { updateOn: 'submit' });

// Create members for easier access of arf controls
Expand Down Expand Up @@ -79,12 +88,25 @@ export class InstructorRequestFormComponent {
return str;
}

/**
* Handles successful completion of reCAPTCHA challenge.
*
* @param captchaResponse user's reCAPTCHA response token.
*/
handleCaptchaSuccess(captchaResponse: string): void {
this.isCaptchaSuccessful = true;
this.captchaResponse = captchaResponse;
}

/**
* Handles form submission.
*/
onSubmit(): void {
this.hasSubmitAttempt = true;
this.isLoading = true;
this.serverErrorMessage = '';

if (this.arf.invalid) {
if (this.arf.invalid || (this.captchaSiteKey && !this.captchaResponse)) {
this.isLoading = false;
// Do not submit form
return;
Expand All @@ -103,6 +125,7 @@ export class InstructorRequestFormComponent {
instructorEmail: email,
instructorName: name,
instructorInstitution: combinedInstitution,
captchaResponse: this.captchaSiteKey ? this.captchaResponse! : '',
};

if (comments) {
Expand Down
2 changes: 2 additions & 0 deletions src/web/app/pages-static/request-page/request-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxCaptchaModule } from 'ngx-captcha';
import { InstructorRequestFormComponent } from './instructor-request-form/instructor-request-form.component';
import { RequestPageComponent } from './request-page.component';
import { TeammatesRouterModule } from '../../components/teammates-router/teammates-router.module';
Expand Down Expand Up @@ -31,6 +32,7 @@ const routes: Routes = [
TeammatesRouterModule,
ReactiveFormsModule,
NgbAlertModule,
NgxCaptchaModule,
],
})
export class RequestPageModule { }

0 comments on commit 6b83e4f

Please sign in to comment.