Skip to content

Commit

Permalink
487 whitelist (#491)
Browse files Browse the repository at this point in the history
* #487 - Initial work for whitelist

* #487 - acquire whitelist items when a reservation is confirmed

* #487 - fix mysql syntax

* #487 - add validation

* #487 - initial version of admin GUI

* #487 - fix test

* #487 - Initial work for whitelist

* #487 - acquire whitelist items when a reservation is confirmed

* #487 - fix mysql syntax

* #487 - add validation

* #487 - initial version of admin GUI

* #487 - fix test

* #487 - integrate check into reservation flow

* #487 - configure whitelist at event level

* #487 - add whitelist deactivation button

* #487 - update UI

* #487 - refactoring whitelist -> attendeeList

* #487 - update list

* #487 - event detail: add info messages

* #487 - rename attendeeList -> group
  • Loading branch information
cbellone authored and syjer committed Jul 22, 2018
1 parent 116f960 commit c73070d
Show file tree
Hide file tree
Showing 40 changed files with 1,905 additions and 75 deletions.
17 changes: 13 additions & 4 deletions src/main/java/alfio/controller/ReservationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import alfio.util.ErrorsCode;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import alfio.util.Validator.AdvancedTicketAssignmentValidator;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
Expand Down Expand Up @@ -67,6 +68,7 @@
import static alfio.model.system.Configuration.getSystemConfiguration;
import static alfio.model.system.ConfigurationKeys.*;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@Controller
@Log4j2
Expand All @@ -90,6 +92,7 @@ public class ReservationController {
private final MollieManager mollieManager;
private final RecaptchaService recaptchaService;
private final TicketReservationRepository ticketReservationRepository;
private final GroupManager groupManager;

@RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/book", method = RequestMethod.GET)
public String showPaymentPage(@PathVariable("eventName") String eventName,
Expand Down Expand Up @@ -455,7 +458,10 @@ public String handleReservation(@PathVariable("eventName") String eventName,
if(!paymentForm.isPostponeAssignment() && !ticketRepository.checkTicketUUIDs(reservationId, paymentForm.getTickets().keySet())) {
bindingResult.reject(ErrorsCode.STEP_2_MISSING_ATTENDEE_DATA);
}
paymentForm.validate(bindingResult, reservationCost, event, ticketFieldRepository.findAdditionalFieldsForEvent(event.getId()), new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), reservationId));
Map<String, Integer> categories = ticketRepository.findTicketsInReservation(reservationId).stream().collect(toMap(Ticket::getUuid, Ticket::getCategoryId));
AdvancedTicketAssignmentValidator advancedValidator = new AdvancedTicketAssignmentValidator(new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), reservationId),
new GroupManager.WhitelistValidator(event.getId(), groupManager));
paymentForm.validate(bindingResult, reservationCost, event, ticketFieldRepository.findAdditionalFieldsForEvent(event.getId()), advancedValidator, categories);
if (bindingResult.hasErrors()) {
ticketReservationRepository.updateTicketReservation(reservationId, ticketReservation.getStatus().name(), paymentForm.getEmail(),
paymentForm.getFullName(), paymentForm.getFirstName(), paymentForm.getLastName(), locale.getLanguage(), paymentForm.getBillingAddress(), null, null, paymentForm.getCustomerReference());
Expand Down Expand Up @@ -503,15 +509,18 @@ public String handleReservation(@PathVariable("eventName") String eventName,
}
//

Map<String, String> ticketEmails = paymentForm.getTickets().entrySet().stream().map(e -> Pair.of(e.getKey(), e.getValue().getEmail())).collect(toMap(Pair::getKey, Pair::getValue));//TODO temporary, to be removed in 2.0

final PaymentResult status = ticketReservationManager.confirm(paymentForm.getToken(), paymentForm.getPaypalPayerID(), event, reservationId, paymentForm.getEmail(),
customerName, locale, paymentForm.getBillingAddress(), paymentForm.getCustomerReference(), reservationCost, SessionUtil.retrieveSpecialPriceSessionId(request),
Optional.ofNullable(paymentForm.getPaymentMethod()), paymentForm.isInvoiceRequested(), paymentForm.getVatCountryCode(),
paymentForm.getVatNr(), optionalReservation.get().getVatStatus(), paymentForm.getTermAndConditionsAccepted(), Optional.ofNullable(paymentForm.getPrivacyPolicyAccepted()).orElse(false));
paymentForm.getVatNr(), optionalReservation.get().getVatStatus(), paymentForm.getTermAndConditionsAccepted(), Optional.ofNullable(paymentForm.getPrivacyPolicyAccepted()).orElse(false), ticketEmails);

if(!status.isSuccessful()) {
String errorMessageCode = status.getErrorCode().get();
String errorMessageCode = status.getErrorCode().orElse("");
MessageSourceResolvable message = new DefaultMessageSourceResolvable(new String[]{errorMessageCode, StripeManager.STRIPE_UNEXPECTED});
bindingResult.reject(ErrorsCode.STEP_2_PAYMENT_PROCESSING_ERROR, new Object[]{messageSource.getMessage(message, locale)}, null);
String errorCode = ErrorsCode.STEP_2_WHITELIST_CHECK_FAILED.equals(errorMessageCode) ? errorMessageCode : ErrorsCode.STEP_2_PAYMENT_PROCESSING_ERROR;
bindingResult.reject(errorCode, new Object[]{messageSource.getMessage(message, locale)}, null);
SessionUtil.addToFlash(bindingResult, redirectAttributes);
return redirectReservation(optionalReservation, eventName, reservationId);
}
Expand Down
159 changes: 159 additions & 0 deletions src/main/java/alfio/controller/api/admin/GroupApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.controller.api.admin;

import alfio.manager.EventManager;
import alfio.manager.GroupManager;
import alfio.manager.user.UserManager;
import alfio.model.group.Group;
import alfio.model.group.LinkedGroup;
import alfio.model.modification.GroupModification;
import alfio.model.modification.LinkedGroupModification;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static alfio.util.OptionalWrapper.optionally;

@RestController
@RequestMapping("/admin/api/group")
@RequiredArgsConstructor
public class GroupApiController {

private final GroupManager groupManager;
private final UserManager userManager;
private final EventManager eventManager;

@GetMapping("/{organizationId}")
public ResponseEntity<List<Group>> loadAllGroupsForOrganization(@PathVariable("organizationId") int organizationId, Principal principal) {
if(notOwner(principal.getName(), organizationId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return ResponseEntity.ok(groupManager.getAllForOrganization(organizationId));
}

@GetMapping("/{organizationId}/detail/{listId}")
public ResponseEntity<GroupModification> loadDetail(@PathVariable("organizationId") int organizationId, @PathVariable("listId") int listId, Principal principal) {
if(notOwner(principal.getName(), organizationId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return groupManager.loadComplete(listId).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}

@PostMapping("/{organizationId}/update/{listId}")
public ResponseEntity<GroupModification> updateGroup(@PathVariable("organizationId") int organizationId,
@PathVariable("listId") int listId,
@RequestBody GroupModification modification,
Principal principal) {
if(notOwner(principal.getName(), organizationId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return groupManager.update(listId, modification).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}

@PostMapping("/{organizationId}/new")
public ResponseEntity<Integer> createNew(@PathVariable("organizationId") int organizationId, @RequestBody GroupModification request, Principal principal) {
if(notOwner(principal.getName(), organizationId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
if(request.getOrganizationId() != organizationId) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(groupManager.createNew(request));
}

@GetMapping("/event/{eventName}/all")
public ResponseEntity<List<LinkedGroup>> findLinked(@PathVariable("eventName") String eventName,
Principal principal) {
return eventManager.getOptionalByName(eventName, principal.getName())
.map(event -> ResponseEntity.ok(groupManager.getLinksForEvent(event.getId())))
.orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/event/{eventName}")
public ResponseEntity<LinkedGroup> findActiveGroup(@PathVariable("eventName") String eventName,
Principal principal) {
return eventManager.getOptionalByName(eventName, principal.getName())
.map(event -> {
Optional<LinkedGroup> configuration = groupManager.getLinksForEvent(event.getId()).stream()
.filter(c -> c.getTicketCategoryId() == null)
.findFirst();
return configuration.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.noContent().build());
})
.orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/event/{eventName}/category/{categoryId}")
public ResponseEntity<LinkedGroup> findActiveGroup(@PathVariable("eventName") String eventName,
@PathVariable("categoryId") int categoryId,
Principal principal) {
return eventManager.getOptionalByName(eventName, principal.getName())
.map(event -> {
Optional<LinkedGroup> configuration = groupManager.findLinks(event.getId(), categoryId)
.stream()
.filter(c -> c.getTicketCategoryId() != null && c.getTicketCategoryId() == categoryId)
.findFirst();
return configuration.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.noContent().build());
})
.orElseGet(() -> ResponseEntity.notFound().build());
}

@PostMapping("/{listId}/link")
public ResponseEntity<Integer> linkGroup(@PathVariable("listId") int listId, @RequestBody LinkedGroupModification body, Principal principal) {
if(body == null || listId != body.getGroupId()) {
return ResponseEntity.badRequest().build();
}

return optionally(() -> eventManager.getSingleEventById(body.getEventId(), principal.getName()))
.map(event -> {
Optional<LinkedGroup> existing = groupManager.getLinksForEvent(event.getId())
.stream()
.filter(c -> c.getGroupId() == listId && Objects.equals(body.getTicketCategoryId(), c.getTicketCategoryId()))
.findFirst();
LinkedGroup link;
if(existing.isPresent()) {
link = groupManager.updateLink(existing.get().getId(), body);
} else {
link = groupManager.createLink(listId, event.getId(), body);
}
return ResponseEntity.ok(link.getId());
})
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/{organizationId}/link/{configurationId}")
public ResponseEntity<String> unlinkGroup(@PathVariable("organizationId") int organizationId, @PathVariable("configurationId") int configurationId, Principal principal) {
if(optionally(() -> userManager.findUserByUsername(principal.getName())).filter(u -> userManager.isOwnerOfOrganization(u, organizationId)).isPresent()) {
groupManager.disableLink(configurationId);
return ResponseEntity.ok("OK");
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

private boolean notOwner(String username, int organizationId) {
return !optionally(() -> userManager.findUserByUsername(username))
.filter(user -> userManager.isOwnerOfOrganization(user, organizationId))
.isPresent();
}

}
17 changes: 12 additions & 5 deletions src/main/java/alfio/controller/api/support/TicketHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import alfio.manager.EuVatChecker;
import alfio.manager.EuVatChecker.SameCountryValidator;
import alfio.manager.FileUploadManager;
import alfio.manager.GroupManager;
import alfio.manager.TicketReservationManager;
import alfio.manager.support.PartialTicketTextGenerator;
import alfio.model.*;
Expand All @@ -35,12 +36,11 @@
import alfio.util.LocaleUtil;
import alfio.util.TemplateManager;
import alfio.util.Validator;
import alfio.util.Validator.AdvancedTicketAssignmentValidator;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
Expand All @@ -54,8 +54,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static alfio.model.TicketFieldConfiguration.Context.ATTENDEE;

@Component
@AllArgsConstructor
public class TicketHelper {
Expand All @@ -71,6 +69,7 @@ public class TicketHelper {
private final TicketFieldRepository ticketFieldRepository;
private final AdditionalServiceItemRepository additionalServiceItemRepository;
private final EuVatChecker vatChecker;
private final GroupManager groupManager;


public List<TicketFieldConfigurationDescriptionAndValue> findTicketFieldConfigurationAndValue(Ticket ticket) {
Expand Down Expand Up @@ -114,7 +113,12 @@ private Triple<ValidationResult, Event, Ticket> assignTicket(UpdateTicketOwnerFo

final TicketReservation ticketReservation = result.getMiddle();
List<TicketFieldConfiguration> fieldConf = ticketFieldRepository.findAdditionalFieldsForEvent(event.getId());
ValidationResult validationResult = Validator.validateTicketAssignment(updateTicketOwner, fieldConf, bindingResult, event, formPrefix, new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), ticketReservation.getId()))
AdvancedTicketAssignmentValidator advancedValidator = new AdvancedTicketAssignmentValidator(new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), ticketReservation.getId()),
new GroupManager.WhitelistValidator(event.getId(), groupManager));

Validator.AdvancedValidationContext context = new Validator.AdvancedValidationContext(updateTicketOwner, fieldConf, t.getCategoryId(), t.getUuid(), formPrefix);
ValidationResult validationResult = Validator.validateTicketAssignment(updateTicketOwner, fieldConf, bindingResult, event, formPrefix)
.or(Validator.performAdvancedValidation(advancedValidator, context, bindingResult.orElse(null)))
.ifSuccess(() -> updateTicketOwner(updateTicketOwner, request, t, event, ticketReservation, userDetails));
return Triple.of(validationResult, event, ticketRepository.findByUUID(t.getUuid()));
}
Expand Down Expand Up @@ -218,6 +222,9 @@ private void updateTicketOwner(UpdateTicketOwnerForm updateTicketOwner, HttpServ
getConfirmationTextBuilder(request, event, ticketReservation, t, category),
getOwnerChangeTextBuilder(request, t, event),
userDetails);
if(t.hasBeenSold() && !groupManager.findLinks(event.getId(), t.getCategoryId()).isEmpty()) {
ticketRepository.forbidReassignment(Collections.singletonList(t.getId()));
}
}

private PartialTicketTextGenerator getOwnerChangeTextBuilder(HttpServletRequest request, Ticket t, Event event) {
Expand Down

0 comments on commit c73070d

Please sign in to comment.