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 { }