Skip to content

Commit 6b83e4f

Browse files
xenosfjayasting98
andauthored
[#11878] Add CAPTCHA to ARF (#13081)
* 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 <[email protected]>
1 parent 76db4cc commit 6b83e4f

File tree

7 files changed

+71
-2
lines changed

7 files changed

+71
-2
lines changed

src/main/java/teammates/ui/request/AccountCreateRequest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class AccountCreateRequest extends BasicRequest {
1818
private String instructorInstitution;
1919
@Nullable
2020
private String instructorComments;
21+
@Nullable
22+
private String captchaResponse;
2123

2224
public String getInstructorEmail() {
2325
return instructorEmail;
@@ -35,6 +37,10 @@ public String getInstructorComments() {
3537
return this.instructorComments;
3638
}
3739

40+
public String getCaptchaResponse() {
41+
return this.captchaResponse;
42+
}
43+
3844
public void setInstructorName(String name) {
3945
this.instructorName = name;
4046
}
@@ -51,6 +57,10 @@ public void setInstructorComments(String instructorComments) {
5157
this.instructorComments = instructorComments;
5258
}
5359

60+
public void setCaptchaResponse(String captchaResponse) {
61+
this.captchaResponse = captchaResponse;
62+
}
63+
5464
@Override
5565
public void validate() throws InvalidHttpRequestBodyException {
5666
assertTrue(this.instructorEmail != null, "email cannot be null");

src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ public JsonResult execute()
3333
throws InvalidHttpRequestBodyException, InvalidOperationException {
3434
AccountCreateRequest createRequest = getAndValidateRequestBody(AccountCreateRequest.class);
3535

36+
if (userInfo == null || !userInfo.isAdmin) {
37+
String userCaptchaResponse = createRequest.getCaptchaResponse();
38+
if (!recaptchaVerifier.isVerificationSuccessful(userCaptchaResponse)) {
39+
throw new InvalidHttpRequestBodyException("Something went wrong with "
40+
+ "the reCAPTCHA verification. Please try again.");
41+
}
42+
}
43+
3644
String instructorName = createRequest.getInstructorName().trim();
3745
String instructorEmail = createRequest.getInstructorEmail().trim();
3846
String instructorInstitution = createRequest.getInstructorInstitution().trim();

src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ exports[`InstructorRequestFormComponent should render correctly 1`] = `
88
STUDENT_NAME_MAX_LENGTH={[Function Number]}
99
accountService={[Function Object]}
1010
arf={[Function FormGroup]}
11+
captchaSiteKey=""
1112
comments={[Function FormControl2]}
1213
country={[Function FormControl2]}
1314
email={[Function FormControl2]}
1415
hasSubmitAttempt="false"
1516
institution={[Function FormControl2]}
17+
isCaptchaSuccessful="false"
1618
isLoading="false"
19+
lang={[Function String]}
1720
name={[Function FormControl2]}
1821
requestSubmissionEvent={[Function EventEmitter_]}
1922
serverErrorMessage=""
23+
size={[Function String]}
2024
>
2125
<p
2226
aria-hidden="true"

src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@
105105
[attr.aria-invalid]="comments.invalid"></textarea>
106106
</div>
107107
<br>
108+
<div *ngIf="captchaSiteKey !== ''" class="form-group">
109+
<ngx-recaptcha2 #captchaElem
110+
[siteKey]="captchaSiteKey"
111+
(success)="handleCaptchaSuccess($event)"
112+
[useGlobalDomain]="false"
113+
[size]="size"
114+
[hl]="lang"
115+
formControlName="recaptcha"
116+
class="{{!isCaptchaSuccessful ? ' is-invalid' : ''}}">
117+
</ngx-recaptcha2>
118+
<div *ngIf="!isCaptchaSuccessful && hasSubmitAttempt" role="alert" tabindex="0"
119+
class="invalid-feedback">
120+
Please complete the CAPTCHA verification.
121+
</div>
122+
<br>
123+
</div>
108124
<ngb-alert type="danger" [dismissible]="false" *ngIf="hasSubmitAttempt && arf.invalid" class="error-box">
109125
<strong>There was a problem with your submission.</strong> Please check and fix the errors above and submit again.
110126
</ngb-alert>

src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22
import { ReactiveFormsModule } from '@angular/forms';
33
import { By } from '@angular/platform-browser';
4+
import { NgxCaptchaModule } from 'ngx-captcha';
45
import { Observable, first } from 'rxjs';
56
import { InstructorRequestFormModel } from './instructor-request-form-model';
67
import { InstructorRequestFormComponent } from './instructor-request-form.component';
@@ -46,7 +47,7 @@ describe('InstructorRequestFormComponent', () => {
4647
beforeEach(waitForAsync(() => {
4748
TestBed.configureTestingModule({
4849
declarations: [InstructorRequestFormComponent],
49-
imports: [ReactiveFormsModule],
50+
imports: [ReactiveFormsModule, NgxCaptchaModule],
5051
providers: [{ provide: AccountService, useValue: accountServiceStub }],
5152
})
5253
.compileComponents();
@@ -56,10 +57,15 @@ describe('InstructorRequestFormComponent', () => {
5657
fixture = TestBed.createComponent(InstructorRequestFormComponent);
5758
component = fixture.componentInstance;
5859
accountService = TestBed.inject(AccountService);
60+
component.captchaSiteKey = ''; // Test ignores captcha
5961
fixture.detectChanges();
6062
jest.clearAllMocks();
6163
});
6264

65+
it('should have empty captcha key', () => {
66+
expect(component).toBeTruthy();
67+
});
68+
6369
it('should create', () => {
6470
expect(component).toBeTruthy();
6571
});

src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, EventEmitter, Output } from '@angular/core';
22
import { FormControl, FormGroup, Validators } from '@angular/forms';
33
import { finalize } from 'rxjs';
44
import { InstructorRequestFormModel } from './instructor-request-form-model';
5+
import { environment } from '../../../../environments/environment';
56
import { AccountService } from '../../../../services/account.service';
67
import { AccountCreateRequest } from '../../../../types/api-request';
78
import { FormValidator } from '../../../../types/form-validator';
@@ -22,6 +23,13 @@ export class InstructorRequestFormComponent {
2223
readonly COUNTRY_NAME_MAX_LENGTH = FormValidator.COUNTRY_NAME_MAX_LENGTH;
2324
readonly EMAIL_MAX_LENGTH = FormValidator.EMAIL_MAX_LENGTH;
2425

26+
// Captcha
27+
captchaSiteKey: string = environment.captchaSiteKey;
28+
isCaptchaSuccessful: boolean = false;
29+
captchaResponse?: string;
30+
size: 'compact' | 'normal' = 'normal';
31+
lang: string = 'en';
32+
2533
arf = new FormGroup({
2634
name: new FormControl('', [
2735
Validators.required,
@@ -44,6 +52,7 @@ export class InstructorRequestFormComponent {
4452
Validators.maxLength(FormValidator.EMAIL_MAX_LENGTH),
4553
]),
4654
comments: new FormControl(''),
55+
recaptcha: new FormControl(''),
4756
}, { updateOn: 'submit' });
4857

4958
// Create members for easier access of arf controls
@@ -79,12 +88,25 @@ export class InstructorRequestFormComponent {
7988
return str;
8089
}
8190

91+
/**
92+
* Handles successful completion of reCAPTCHA challenge.
93+
*
94+
* @param captchaResponse user's reCAPTCHA response token.
95+
*/
96+
handleCaptchaSuccess(captchaResponse: string): void {
97+
this.isCaptchaSuccessful = true;
98+
this.captchaResponse = captchaResponse;
99+
}
100+
101+
/**
102+
* Handles form submission.
103+
*/
82104
onSubmit(): void {
83105
this.hasSubmitAttempt = true;
84106
this.isLoading = true;
85107
this.serverErrorMessage = '';
86108

87-
if (this.arf.invalid) {
109+
if (this.arf.invalid || (this.captchaSiteKey && !this.captchaResponse)) {
88110
this.isLoading = false;
89111
// Do not submit form
90112
return;
@@ -103,6 +125,7 @@ export class InstructorRequestFormComponent {
103125
instructorEmail: email,
104126
instructorName: name,
105127
instructorInstitution: combinedInstitution,
128+
captchaResponse: this.captchaSiteKey ? this.captchaResponse! : '',
106129
};
107130

108131
if (comments) {

src/web/app/pages-static/request-page/request-page.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
33
import { ReactiveFormsModule } from '@angular/forms';
44
import { RouterModule, Routes } from '@angular/router';
55
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
6+
import { NgxCaptchaModule } from 'ngx-captcha';
67
import { InstructorRequestFormComponent } from './instructor-request-form/instructor-request-form.component';
78
import { RequestPageComponent } from './request-page.component';
89
import { TeammatesRouterModule } from '../../components/teammates-router/teammates-router.module';
@@ -31,6 +32,7 @@ const routes: Routes = [
3132
TeammatesRouterModule,
3233
ReactiveFormsModule,
3334
NgbAlertModule,
35+
NgxCaptchaModule,
3436
],
3537
})
3638
export class RequestPageModule { }

0 commit comments

Comments
 (0)