Skip to content
This repository was archived by the owner on Aug 12, 2023. It is now read-only.

Commit

Permalink
Add captcha to prevent spam (#33)
Browse files Browse the repository at this point in the history
* Started work on #32

Signed-off-by: Jurriaan Den Toonder <[email protected]>

* Add captcha to feedback, fixes #32

Signed-off-by: Jurriaan Den Toonder <[email protected]>

* Remove large if blocks

* Removed application.yml from versioning and added example yml file

Signed-off-by: Jurriaan Den Toonder <[email protected]>

* Add google guava for ImmutableMap in EducationFeedbackController

Signed-off-by: Jurriaan Den Toonder <[email protected]>
  • Loading branch information
Fastjur authored and svenpopping committed Mar 13, 2019
1 parent 78c7e98 commit c7f7228
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 45 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ out/
!src/main/resources/example-properties.properties
!gradle-wrapper.properties

application.yml

.gradle/
config/
!src/main/java/ch/wisv/config/
Expand Down
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jar {
}

sourceCompatibility = 11
targetCompatibility = 11

repositories {
mavenCentral()
Expand Down Expand Up @@ -60,8 +61,10 @@ dependencies {
compile 'org.webjars.bower:wisvch-bootstrap-theme:4.0.0'
compile 'org.webjars:font-awesome:5.4.1'
compile 'org.webjars:jquery:3.3.1-1'
compile 'org.webjars:awesomplete:6328816'
compile 'org.webjars:awesomplete:1.1.4'
compile 'org.webjars:datatables:1.10.19'

compile 'com.google.guava:guava:27.1-jre'
}

eclipse {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package ch.wisv.controller;

import ch.wisv.domain.feedback.AssociationFeedback;
import ch.wisv.domain.feedback.EducationFeedback;
import ch.wisv.service.AssociationFeedbackService;
import ch.wisv.service.CaptchaService;
import ch.wisv.service.NotificationService;
import ch.wisv.util.BindingResultBuilder;
import java.util.HashMap;
import javax.transaction.Transactional;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -13,6 +17,7 @@
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

/**
Expand All @@ -27,22 +32,31 @@ public class AssociationFeedbackController {
private AssociationFeedbackService associationFeedbackService;
/** Service for mail notifications. */
private NotificationService notificationService;
/** Service to handle captcha validation */
private final CaptchaService captchaService;

/**
* Autowired constructor.
*/
@Autowired
public AssociationFeedbackController(AssociationFeedbackService associationFeedbackService, NotificationService notificationService) {
public AssociationFeedbackController(AssociationFeedbackService associationFeedbackService, NotificationService notificationService, CaptchaService captchaService) {
this.associationFeedbackService = associationFeedbackService;
this.notificationService = notificationService;
this.captchaService = captchaService;
}

/**
* Create new association feedback.
*/
@GetMapping("/create")
public String create(Model model) {
model.addAttribute("feedback", new AssociationFeedback());
if (!model.containsAttribute("feedback")) {
model.addAttribute("feedback", new EducationFeedback());
}

if (!model.containsAttribute("errors")) {
model.addAttribute("errors", new HashMap<String, String>());
}

return "association/associationForm";
}
Expand All @@ -56,19 +70,23 @@ public String save(
@Valid @ModelAttribute("feedback") AssociationFeedback associationFeedback,
BindingResult bindingResult,
RedirectAttributes redirectAttributes,
Model model
@RequestParam(value="g-recaptcha-response") String clientResponse
) {
if (bindingResult.hasErrors()) {
model.addAttribute("feedback", associationFeedback);

return "association/associationForm";
redirectAttributes.addFlashAttribute("errors", BindingResultBuilder.createErrorMap(bindingResult));
} else if (!captchaService.validateCaptcha(clientResponse)) {
redirectAttributes.addFlashAttribute("captchaError", true);
} else {
associationFeedbackService.save(associationFeedback);
notificationService.sendNotifications(associationFeedback);
redirectAttributes.addFlashAttribute("message", "Thanks! Your feedback has been submitted." +
" If you filled in your email, you will find a copy of your feedback in your mail.");

return "redirect:/association/create";
associationFeedback = new AssociationFeedback();
}

redirectAttributes.addFlashAttribute("feedback", associationFeedback);

return "redirect:/association/create";
}
}
60 changes: 36 additions & 24 deletions src/main/java/ch/wisv/controller/EducationFeedbackController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

import ch.wisv.domain.course.Course;
import ch.wisv.domain.feedback.EducationFeedback;
import ch.wisv.service.CaptchaService;
import ch.wisv.service.CourseService;
import ch.wisv.service.EducationFeedbackService;
import ch.wisv.service.NotificationService;
import javax.transaction.Transactional;
import javax.validation.Valid;
import ch.wisv.util.BindingResultBuilder;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.transaction.Transactional;
import javax.validation.Valid;
import java.util.HashMap;

/**
* Controller for education feedback.
*
Expand All @@ -31,23 +33,33 @@ public class EducationFeedbackController {
private CourseService courseService;
/** Service for mail notifications. */
private NotificationService notificationService;
/** Service to handle captcha validation */
private final CaptchaService captchaService;

/**
* Autowired constructor.
*/
@Autowired
public EducationFeedbackController(EducationFeedbackService educationFeedbackService, CourseService courseService, NotificationService notificationService) {
public EducationFeedbackController(EducationFeedbackService educationFeedbackService, CourseService courseService, NotificationService notificationService, CaptchaService captchaService) {
this.educationFeedbackService = educationFeedbackService;
this.courseService = courseService;
this.notificationService = notificationService;
this.captchaService = captchaService;
}

/**
* Create new education feedback.
*/
@GetMapping("/create")
public String create(Model model) {
model.addAttribute("feedback", new EducationFeedback());
if (!model.containsAttribute("feedback")) {
model.addAttribute("feedback", new EducationFeedback());
}

if (!model.containsAttribute("errors")) {
model.addAttribute("errors", new HashMap<String, String>());
}

model.addAttribute("courses", courseService.list());

return "education/educationForm";
Expand All @@ -61,28 +73,28 @@ public String create(Model model) {
public String save(
@Valid @ModelAttribute("feedback") EducationFeedback educationFeedback,
BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model
RedirectAttributes redirectAttributes,
@RequestParam(value="g-recaptcha-response") String clientResponse
) {
Course course = courseService.get(educationFeedback.getCourseCode().toUpperCase());
if (course == null) {
model.addAttribute("courseCodeError", "");
model.addAttribute("courses", courseService.list());
model.addAttribute("feedback", educationFeedback);

return "education/educationForm";
}
if(bindingResult.hasErrors()) {
model.addAttribute("courses", courseService.list());
model.addAttribute("feedback", educationFeedback);
if (course == null) {
redirectAttributes.addFlashAttribute("errors", ImmutableMap.of("courseCode", true));
} else if (bindingResult.hasErrors()) {
redirectAttributes.addFlashAttribute("errors", BindingResultBuilder.createErrorMap(bindingResult));
} else if (!captchaService.validateCaptcha(clientResponse)) {
redirectAttributes.addFlashAttribute("captchaError", true);
} else {
educationFeedback.setCourse(course);
educationFeedbackService.save(educationFeedback);
notificationService.sendNotifications(educationFeedback);
redirectAttributes.addFlashAttribute("message", "Thanks! Your feedback has been submitted." +
" If you filled in your email, you will find a copy of your feedback in your mail.");

return "education/educationForm";
educationFeedback = new EducationFeedback();
}

educationFeedback.setCourse(course);
educationFeedbackService.save(educationFeedback);
notificationService.sendNotifications(educationFeedback);
redirectAttributes.addFlashAttribute("message", "Thanks! Your feedback has been submitted." +
" If you filled in your email, you will find a copy of your feedback in your mail.");
redirectAttributes.addFlashAttribute("feedback", educationFeedback);

return "redirect:/education/create";
}
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/ch/wisv/service/CaptchaService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ch.wisv.service;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

@Service
public class CaptchaService {

private static Pattern RESPONSE_PATTERN = Pattern.compile("[A-Za-z0-9_-]+");

@Getter
@Value("${wisvch.recaptcha.secret}")
private String secret;

@Getter
@Value("${wisvch.recaptcha.verifyURI}")
private String verifyURI;

private RestTemplateBuilder restTemplateBuilder;

@Autowired
public CaptchaService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplateBuilder = restTemplateBuilder;
}

public boolean validateCaptcha(String clientResponse) {
if (!responseSanityCheck(clientResponse)) {
return false;
}
Map<String, String> body = new HashMap<>();
body.put("secret", getSecret());
body.put("response", clientResponse);

ResponseEntity<Map> recaptchaResponseEntity = restTemplateBuilder.build()
.postForEntity(getVerifyURI() + "?secret={secret}&response={response}", body, Map.class, body);

Map<String, Object> responseBody = recaptchaResponseEntity.getBody();

return (boolean) responseBody.get("success");
}

private boolean responseSanityCheck(String response) {
return StringUtils.hasLength(response) && RESPONSE_PATTERN.matcher(response).matches();
}

}
34 changes: 34 additions & 0 deletions src/main/java/ch/wisv/util/BindingResultBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ch.wisv.util;

import java.util.HashMap;
import java.util.Map;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

/**
* BindingResultBuilder util class.
*/
public final class BindingResultBuilder {

/**
* Private constructor.
*/
private BindingResultBuilder() {
}

/**
* Convert BindingResults into ErrorMap.
*
* @param bindingResult of type BindingResult
*
* @return Map
*/
public static Map<String, String> createErrorMap(BindingResult bindingResult) {
Map<String, String> errorMessages = new HashMap<>();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
errorMessages.put(fieldError.getField(), fieldError.getDefaultMessage());
}

return errorMessages;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ wisvch:
- chbeheer
- bestuur
- vc
recaptcha:
site: [YOUR SITE KEY]
secret: [YOUR SECRET KEY]
verifyURI: https://www.google.com/recaptcha/api/siteverify

mailNotifications:
from: [email protected]
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/static/css/wisvch-feedbacktool.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@

.awesomplete {
width: 100%;
}

.g-recaptcha {
margin-bottom: 14px;
}
15 changes: 15 additions & 0 deletions src/main/resources/templates/association/associationForm.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
layout:decorator="layouts/main" xmlns:layout="http://www.w3.org/1999/xhtml">
<head>
<title>CH FeedbackTool</title>
<script src="https://www.google.com/recaptcha/api.js" async="async" defer="defer"></script>
</head>
<body>

Expand Down Expand Up @@ -86,6 +87,20 @@ <h4 class="display-4">Write your feedback on association</h4>
</div>
</div>

<div class="row">
<div class="col-12 col-md-9 offset-md-3">
<div class="g-recaptcha"
th:attr="data-sitekey=${@environment.getProperty('wisvch.recaptcha.site')}"
></div>
</div>
</div>

<div class="row" th:if="${captchaError}">
<div class="col-12 col-md-9 offset-md-3">
<div class="alert alert-warning">Invalid captcha. Please try again.</div>
</div>
</div>

<div class="row">
<div class="col-12 col-md-9 offset-md-3">
<button type="submit" class="btn btn-secondary">Submit feedback</button>
Expand Down
Loading

0 comments on commit c7f7228

Please sign in to comment.