diff --git a/src/main/java/teammates/ui/request/AccountCreateRequest.java b/src/main/java/teammates/ui/request/AccountCreateRequest.java
index 3e63e750b9b..06f6ff555e8 100644
--- a/src/main/java/teammates/ui/request/AccountCreateRequest.java
+++ b/src/main/java/teammates/ui/request/AccountCreateRequest.java
@@ -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;
@@ -35,6 +37,10 @@ public String getInstructorComments() {
return this.instructorComments;
}
+ public String getCaptchaResponse() {
+ return this.captchaResponse;
+ }
+
public void setInstructorName(String name) {
this.instructorName = name;
}
@@ -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");
diff --git a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java
index f8ba4d571c0..8a622552f40 100644
--- a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java
+++ b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java
@@ -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();
diff --git a/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap b/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap
index 46dcb75b2cf..ddb6374364f 100644
--- a/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap
+++ b/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap
@@ -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]}
>
+
There was a problem with your submission. Please check and fix the errors above and submit again.
diff --git a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts
index 3a5f4fba2b8..a0c9bbac2f2 100644
--- a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts
+++ b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts
@@ -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';
@@ -46,7 +47,7 @@ describe('InstructorRequestFormComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [InstructorRequestFormComponent],
- imports: [ReactiveFormsModule],
+ imports: [ReactiveFormsModule, NgxCaptchaModule],
providers: [{ provide: AccountService, useValue: accountServiceStub }],
})
.compileComponents();
@@ -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();
});
diff --git a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts
index 02b2ae00fd2..7caa14e3bd9 100644
--- a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts
+++ b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts
@@ -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';
@@ -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,
@@ -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
@@ -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;
@@ -103,6 +125,7 @@ export class InstructorRequestFormComponent {
instructorEmail: email,
instructorName: name,
instructorInstitution: combinedInstitution,
+ captchaResponse: this.captchaSiteKey ? this.captchaResponse! : '',
};
if (comments) {
diff --git a/src/web/app/pages-static/request-page/request-page.module.ts b/src/web/app/pages-static/request-page/request-page.module.ts
index 12a9d337875..1d9e96fdbc8 100644
--- a/src/web/app/pages-static/request-page/request-page.module.ts
+++ b/src/web/app/pages-static/request-page/request-page.module.ts
@@ -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';
@@ -31,6 +32,7 @@ const routes: Routes = [
TeammatesRouterModule,
ReactiveFormsModule,
NgbAlertModule,
+ NgxCaptchaModule,
],
})
export class RequestPageModule { }