From d3a22687ed47ae3aae187932a368fef808bc6199 Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Fri, 5 Nov 2021 09:49:58 +0100 Subject: [PATCH 1/4] WIP --- .../v1/admin/ReservationApiV1Controller.java | 115 ++++++++++++ .../api/v2/user/EventApiV2Controller.java | 18 +- .../controller/form/ReservationCreate.java | 29 +++ .../controller/form/ReservationForm.java | 119 +------------ .../controller/support/TemplateProcessor.java | 3 +- .../manager/AdminReservationManager.java | 2 +- .../manager/PromoCodeRequestManager.java | 5 +- .../manager/TicketReservationManager.java | 58 ++++-- .../alfio/manager/WaitingQueueManager.java | 2 +- .../v1/admin/ReservationCreationRequest.java | 76 ++++++++ .../model/api/v1/admin/ReservationUser.java | 61 +++++++ .../TicketReservationModification.java | 16 +- .../alfio/repository/TicketRepository.java | 8 +- src/main/java/alfio/util/ReservationUtil.java | 166 ++++++++++++++++++ .../manager/TicketReservationManagerTest.java | 10 +- 15 files changed, 532 insertions(+), 156 deletions(-) create mode 100644 src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java create mode 100644 src/main/java/alfio/controller/form/ReservationCreate.java create mode 100644 src/main/java/alfio/model/api/v1/admin/ReservationCreationRequest.java create mode 100644 src/main/java/alfio/model/api/v1/admin/ReservationUser.java create mode 100644 src/main/java/alfio/util/ReservationUtil.java diff --git a/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java new file mode 100644 index 0000000000..b2bfc5436c --- /dev/null +++ b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java @@ -0,0 +1,115 @@ +/** + * 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 . + */ +package alfio.controller.api.v1.admin; + +import alfio.manager.EventManager; +import alfio.manager.PromoCodeRequestManager; +import alfio.manager.TicketReservationManager; +import alfio.manager.system.ConfigurationManager; +import alfio.model.api.v1.admin.ReservationCreationRequest; +import alfio.model.result.ErrorCode; +import alfio.util.ReservationUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNullElseGet; + +@RestController +@RequestMapping("/api/v1/admin/event") +public class ReservationApiV1Controller { + + private final TicketReservationManager ticketReservationManager; + private final ConfigurationManager configurationManager; + private final EventManager eventManager; + private final PromoCodeRequestManager promoCodeRequestManager; + + @Autowired + public ReservationApiV1Controller(TicketReservationManager ticketReservationManager, + ConfigurationManager configurationManager, + EventManager eventManager, + PromoCodeRequestManager promoCodeRequestManager) { + this.ticketReservationManager = ticketReservationManager; + this.configurationManager = configurationManager; + this.eventManager = eventManager; + this.promoCodeRequestManager = promoCodeRequestManager; + } + + @PostMapping("/{slug}/reservation") + public ResponseEntity createReservation(@PathVariable("slug") String eventSlug, + @RequestBody ReservationCreationRequest reservationCreationRequest, + Principal principal) { + var bindingResult = new BeanPropertyBindingResult(reservationCreationRequest, "reservation"); + + var optionalEvent = eventManager.getOptionalByName(eventSlug, principal.getName()); + if (optionalEvent.isEmpty()) { + return ResponseEntity.notFound().build(); + } + var event = optionalEvent.get(); + Optional promoCodeDiscount = ReservationUtil.checkPromoCode(reservationCreationRequest, event, promoCodeRequestManager, bindingResult); + var locale = Locale.forLanguageTag(requireNonNullElseGet(reservationCreationRequest.getLanguage(), () -> event.getContentLanguages().get(0).getLanguage())); + var selected = ReservationUtil.validateCreateRequest(reservationCreationRequest, bindingResult, ticketReservationManager, eventManager, "", event); + if(selected.isPresent() && !bindingResult.hasErrors()) { + var pair = selected.get(); + return ticketReservationManager.createTicketReservation(event, pair.getLeft(), pair.getRight(), promoCodeDiscount, locale, bindingResult, principal) + .map(id -> ResponseEntity.ok(CreationResponse.success(id, ticketReservationManager.reservationUrl(id, event)))) + .orElseGet(() -> ResponseEntity.badRequest().build()); + } else { + return ResponseEntity.badRequest() + .body(CreationResponse.error(bindingResult.getAllErrors().stream().map(err -> ErrorCode.custom("invalid."+err.getObjectName(), err.getCode())).collect(Collectors.toList()))); + } + } + + static class CreationResponse { + private final String id; + private final String href; + private final List errors; + + private CreationResponse(String id, String href, List errors) { + this.id = id; + this.href = href; + this.errors = errors; + } + + public String getId() { + return id; + } + + public String getHref() { + return href; + } + + public List getErrors() { + return errors; + } + + static CreationResponse success(String id, String href) { + return new CreationResponse(id, href, null); + } + + static CreationResponse error(List errors) { + return new CreationResponse(null, null, errors); + } + } +} diff --git a/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java b/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java index d4a00eabc7..6959c2b373 100644 --- a/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java +++ b/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java @@ -302,17 +302,7 @@ public ResponseEntity> reserveTickets(@PathVariable("e Locale locale = LocaleUtil.forLanguageTag(lang, event); - Optional, Optional>>> codeCheck = Optional.empty(); - - if(StringUtils.trimToNull(reservation.getPromoCode()) != null) { - var resCheck = promoCodeRequestManager.checkCode(event, reservation.getPromoCode()); - if(!resCheck.isSuccess()) { - bindingResult.reject(ErrorsCode.STEP_1_CODE_NOT_FOUND, ErrorsCode.STEP_1_CODE_NOT_FOUND); - } - codeCheck = Optional.of(resCheck); - } - - Optional promoCodeDiscount = codeCheck.map(ValidatedResponse::getValue).flatMap(Pair::getRight).map(PromoCodeDiscount::getPromoCode); + Optional promoCodeDiscount = ReservationUtil.checkPromoCode(reservation, event, promoCodeRequestManager, bindingResult);; var configurationValues = configurationManager.getFor(List.of( ENABLE_CAPTCHA_FOR_TICKET_SELECTION, RECAPTCHA_API_KEY), event.getConfigurationLevel()); @@ -340,7 +330,7 @@ private Optional createTicketReservation(ReservationForm reservation, Locale locale, Optional promoCodeDiscount, Principal principal) { - return reservation.validate(bindingResult, ticketReservationManager, eventManager, promoCodeDiscount.orElse(null), event) + return ReservationUtil.validateCreateRequest(reservation, bindingResult, ticketReservationManager, eventManager, promoCodeDiscount.orElse(null), event) .flatMap(selected -> ticketReservationManager.createTicketReservation(event, selected.getLeft(), selected.getRight(), promoCodeDiscount, locale, bindingResult, principal)); } @@ -371,8 +361,8 @@ public ResponseEntity checkDiscount(@PathVariable("eventName") return eventRepository.findOptionalByShortName(eventName) .flatMap(event -> { Map quantityByCategory = reservation.getReservation().stream() - .filter(trm -> trm.getAmount() > 0) - .collect(groupingBy(TicketReservationModification::getTicketCategoryId, summingLong(TicketReservationModification::getAmount))); + .filter(trm -> trm.getQuantity() > 0) + .collect(groupingBy(TicketReservationModification::getTicketCategoryId, summingLong(TicketReservationModification::getQuantity))); if(quantityByCategory.isEmpty() || ticketCategoryRepository.countPaidCategoriesInReservation(quantityByCategory.keySet()) == 0) { return Optional.empty(); } diff --git a/src/main/java/alfio/controller/form/ReservationCreate.java b/src/main/java/alfio/controller/form/ReservationCreate.java new file mode 100644 index 0000000000..5565cab768 --- /dev/null +++ b/src/main/java/alfio/controller/form/ReservationCreate.java @@ -0,0 +1,29 @@ +/** + * 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 . + */ +package alfio.controller.form; + +import alfio.model.modification.AdditionalServiceReservationModification; +import alfio.model.modification.TicketReservationModification; + +import java.util.List; + +public interface ReservationCreate { + String getPromoCode(); + List getTickets(); + List getAdditionalServices(); + String getCaptcha(); +} diff --git a/src/main/java/alfio/controller/form/ReservationForm.java b/src/main/java/alfio/controller/form/ReservationForm.java index f403ac9aa7..ea165c9c24 100644 --- a/src/main/java/alfio/controller/form/ReservationForm.java +++ b/src/main/java/alfio/controller/form/ReservationForm.java @@ -16,134 +16,29 @@ */ package alfio.controller.form; -import alfio.controller.decorator.SaleableTicketCategory; -import alfio.manager.EventManager; -import alfio.manager.TicketReservationManager; -import alfio.model.AdditionalService; -import alfio.model.Event; -import alfio.model.SpecialPrice; -import alfio.model.TicketCategory; -import alfio.model.modification.ASReservationWithOptionalCodeModification; import alfio.model.modification.AdditionalServiceReservationModification; import alfio.model.modification.TicketReservationModification; -import alfio.model.modification.TicketReservationWithOptionalCodeModification; -import alfio.util.ClockProvider; -import alfio.util.ErrorsCode; import lombok.Data; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.validation.Errors; import java.io.Serializable; -import java.math.BigDecimal; -import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; //step 1 : choose tickets @Data -public class ReservationForm implements Serializable { +public class ReservationForm implements Serializable, ReservationCreate { private String promoCode; private List reservation; private List additionalService; private String captcha; - private List selected() { - return ofNullable(reservation) - .orElse(emptyList()) - .stream() - .filter(e -> e != null && e.getAmount() != null && e.getTicketCategoryId() != null && e.getAmount() > 0) - .collect(toList()); - } - - private List selectedAdditionalServices() { - return ofNullable(additionalService) - .orElse(emptyList()) - .stream() - .filter(e -> e != null && e.getQuantity() != null && e.getAdditionalServiceId() != null && e.getQuantity() > 0) - .collect(toList()); - } - - private int ticketSelectionCount() { - return selected().stream().mapToInt(TicketReservationModification::getAmount).sum(); - } - - public Optional, List>> validate(Errors bindingResult, - TicketReservationManager tickReservationManager, - EventManager eventManager, - String promoCodeDiscount, - Event event) { - int selectionCount = ticketSelectionCount(); - - if (selectionCount <= 0) { - bindingResult.reject(ErrorsCode.STEP_1_SELECT_AT_LEAST_ONE); - return Optional.empty(); - } - - List> maxTicketsByTicketReservation = selected().stream() - .map(r -> Pair.of(r, tickReservationManager.maxAmountOfTicketsForCategory(event, r.getTicketCategoryId(), promoCodeDiscount))) - .collect(toList()); - Optional> error = maxTicketsByTicketReservation.stream() - .filter(p -> p.getKey().getAmount() > p.getValue()) - .findAny(); - - if(error.isPresent()) { - bindingResult.reject(ErrorsCode.STEP_1_OVER_MAXIMUM, new Object[] { error.get().getValue() }, null); - return Optional.empty(); - } - - final List categories = selected(); - final List additionalServices = selectedAdditionalServices(); - - final boolean validCategorySelection = categories.stream().allMatch(c -> { - TicketCategory tc = eventManager.getTicketCategoryById(c.getTicketCategoryId(), event.getId()); - return eventManager.eventExistsById(tc.getEventId()); - }); - - - final boolean validAdditionalServiceSelected = additionalServices.stream().allMatch(asm -> { - AdditionalService as = eventManager.getAdditionalServiceById(asm.getAdditionalServiceId(), event.getId()); - ZonedDateTime now = event.now(ClockProvider.clock()); - return as.getInception(event.getZoneId()).isBefore(now) && - as.getExpiration(event.getZoneId()).isAfter(now) && - asm.getQuantity() >= 0 && - ((as.isFixPrice() && asm.isQuantityValid(as, selectionCount)) || (!as.isFixPrice() && asm.getAmount() != null && asm.getAmount().compareTo(BigDecimal.ZERO) >= 0)) && - eventManager.eventExistsById(as.getEventId()); - }); - - if(!validCategorySelection || !validAdditionalServiceSelected) { - bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE); - return Optional.empty(); - } - - List res = new ArrayList<>(); - // - Optional specialCode = Optional.ofNullable(StringUtils.trimToNull(promoCode)) - .flatMap(tickReservationManager::getSpecialPriceByCode); - // - final ZonedDateTime now = event.now(ClockProvider.clock()); - maxTicketsByTicketReservation.forEach(pair -> validateCategory(bindingResult, tickReservationManager, eventManager, event, pair.getRight(), res, specialCode, now, pair.getLeft())); - return bindingResult.hasErrors() ? Optional.empty() : Optional.of(Pair.of(res, additionalServices.stream().map(as -> new ASReservationWithOptionalCodeModification(as, specialCode)).collect(Collectors.toList()))); + @Override + public List getTickets() { + return reservation; } - private static void validateCategory(Errors bindingResult, TicketReservationManager tickReservationManager, EventManager eventManager, - Event event, int maxAmountOfTicket, List res, - Optional specialCode, ZonedDateTime now, TicketReservationModification r) { - TicketCategory tc = eventManager.getTicketCategoryById(r.getTicketCategoryId(), event.getId()); - SaleableTicketCategory ticketCategory = new SaleableTicketCategory(tc, now, event, tickReservationManager.countAvailableTickets(event, tc), maxAmountOfTicket, null); - - if (!ticketCategory.getSaleable()) { - bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE); - return; - } - - res.add(new TicketReservationWithOptionalCodeModification(r, ticketCategory.isAccessRestricted() ? specialCode : Optional.empty())); + @Override + public List getAdditionalServices() { + return additionalService; } } diff --git a/src/main/java/alfio/controller/support/TemplateProcessor.java b/src/main/java/alfio/controller/support/TemplateProcessor.java index 3f923c484c..3433c254ba 100644 --- a/src/main/java/alfio/controller/support/TemplateProcessor.java +++ b/src/main/java/alfio/controller/support/TemplateProcessor.java @@ -29,6 +29,7 @@ import ch.digitalfondue.jfiveparse.W3CDom; import com.openhtmltopdf.extend.FSStream; import com.openhtmltopdf.extend.FSStreamFactory; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver; import com.openhtmltopdf.pdfboxout.PdfBoxRenderer; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; import lombok.extern.log4j.Log4j2; @@ -119,7 +120,7 @@ public static void renderToPdf(String page, OutputStream os, ExtensionManager ex try (PdfBoxRenderer renderer = builder.buildPdfRenderer()) { File defaultFont = ImageUtil.getDejaVuSansMonoFont(); if (defaultFont != null) { - renderer.getFontResolver().addFont(defaultFont, "DejaVu Sans Mono", null, null, false); + renderer.getFontResolver().addFont(defaultFont, "DejaVu Sans Mono", null, null, false, PdfBoxFontResolver.FontGroup.MAIN); } renderer.layout(); renderer.createPDF(); diff --git a/src/main/java/alfio/manager/AdminReservationManager.java b/src/main/java/alfio/manager/AdminReservationManager.java index 96359d5a21..0f19caab37 100644 --- a/src/main/java/alfio/manager/AdminReservationManager.java +++ b/src/main/java/alfio/manager/AdminReservationManager.java @@ -527,7 +527,7 @@ private void assignTickets(Event event, } } specialPriceIterator.map(Iterator::next) - .ifPresent(code -> ticketRepository.reserveTicket(reservationId, ticketId, code.getId(), userLanguage, srcPriceCts, event.getCurrency(), event.getVatStatus())); + .ifPresent(code -> ticketRepository.reserveTicket(reservationId, ticketId, code.getId(), userLanguage, srcPriceCts, event.getCurrency(), event.getVatStatus(), null)); } } diff --git a/src/main/java/alfio/manager/PromoCodeRequestManager.java b/src/main/java/alfio/manager/PromoCodeRequestManager.java index 12206f9227..3253f8f744 100644 --- a/src/main/java/alfio/manager/PromoCodeRequestManager.java +++ b/src/main/java/alfio/manager/PromoCodeRequestManager.java @@ -30,6 +30,7 @@ import alfio.util.ClockProvider; import alfio.util.ErrorsCode; import alfio.util.RequestUtils; +import alfio.util.ReservationUtil; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -193,7 +194,7 @@ private Pair, BindingResult> makeSimpleReservation(Event event, ReservationForm form = new ReservationForm(); form.setPromoCode(promoCode); TicketReservationModification reservation = new TicketReservationModification(); - reservation.setAmount(1); + reservation.setQuantity(1); reservation.setTicketCategoryId(ticketCategoryId); form.setReservation(Collections.singletonList(reservation)); var bindingRes = new BeanPropertyBindingResult(form, "reservationForm"); @@ -206,7 +207,7 @@ private Optional createTicketReservation(ReservationForm reservation, Locale locale, Optional promoCodeDiscount, Principal principal) { - return reservation.validate(bindingResult, ticketReservationManager, eventManager, promoCodeDiscount.orElse(null), event) + return ReservationUtil.validateCreateRequest(reservation, bindingResult, ticketReservationManager, eventManager, promoCodeDiscount.orElse(null), event) .flatMap(selected -> ticketReservationManager.createTicketReservation(event, selected.getLeft(), selected.getRight(), promoCodeDiscount, locale, bindingResult, principal)); } diff --git a/src/main/java/alfio/manager/TicketReservationManager.java b/src/main/java/alfio/manager/TicketReservationManager.java index 31b14896d9..2b8a02da26 100644 --- a/src/main/java/alfio/manager/TicketReservationManager.java +++ b/src/main/java/alfio/manager/TicketReservationManager.java @@ -45,6 +45,7 @@ import alfio.model.decorator.TicketPriceContainer; import alfio.model.extension.CustomEmailText; import alfio.model.group.LinkedGroup; +import alfio.model.metadata.TicketMetadata; import alfio.model.modification.ASReservationWithOptionalCodeModification; import alfio.model.modification.AdditionalServiceReservationModification; import alfio.model.modification.TicketReservationWithOptionalCodeModification; @@ -338,7 +339,7 @@ public String createTicketReservation(Event event, int ticketCount = list .stream() - .map(TicketReservationWithOptionalCodeModification::getAmount) + .map(TicketReservationWithOptionalCodeModification::getQuantity) .mapToInt(Integer::intValue).sum(); // apply valid additional service with supplement policy mandatory one for ticket @@ -383,7 +384,7 @@ Optional createDynamicPromoCodeIfNeeded(Event event, List reservedForUpdate = reserveTickets(event.getId(), ticketReservation, forWaitingQueue ? asList(TicketStatus.RELEASED, TicketStatus.PRE_RESERVED) : singletonList(TicketStatus.FREE)); - int requested = ticketReservation.getAmount(); + int requested = ticketReservation.getQuantity(); if (reservedForUpdate.size() != requested) { throw new NotEnoughTicketsException(); } @@ -435,29 +436,47 @@ void reserveTicketsForCategory(Event event, } AtomicInteger counter = new AtomicInteger(0); - var ticketsAndSpecialPrices = specialPrices.stream().map(sp -> Pair.of(reservedForUpdate.get(counter.getAndIncrement()), sp)).collect(Collectors.toList()); + List> ticketMetadata = requireNonNullElse(ticketReservation.getMetadata(), List.of()); + var ticketsAndSpecialPrices = specialPrices.stream() + .map(sp -> { + int index = counter.getAndIncrement(); + return Triple.of(reservedForUpdate.get(index), sp, getAtIndexOrNull(ticketMetadata, index)); + }).collect(Collectors.toList()); if(specialPrices.size() == 1) { var ticketId = reservedForUpdate.get(0); var sp = specialPrices.get(0); var accessCodeId = accessCodeOrDiscount != null && accessCodeOrDiscount.getHiddenCategoryId() != null ? accessCodeOrDiscount.getId() : null; + TicketMetadata metadata = null; + var attributes = getAtIndexOrNull(ticketMetadata, 0); + if(attributes != null) { + metadata = new TicketMetadata(null, null, attributes); + } ticketRepository.reserveTicket(reservationId, ticketId, sp.getId(), locale.getLanguage(), category.getSrcPriceCts(), category.getCurrencyCode(), - event.getVatStatus()); + event.getVatStatus(), + metadata); specialPriceRepository.updateStatus(sp.getId(), Status.PENDING.toString(), null, accessCodeId); } else { jdbcTemplate.batchUpdate(ticketRepository.batchReserveTicket(), ticketsAndSpecialPrices.stream().map( - pair -> new MapSqlParameterSource("reservationId", reservationId) - .addValue("ticketId", pair.getKey()) - .addValue("specialCodeId", pair.getValue().getId()) - .addValue("userLanguage", locale.getLanguage()) - .addValue("srcPriceCts", category.getSrcPriceCts()) - .addValue("currencyCode", category.getCurrencyCode()) - .addValue("vatStatus", event.getVatStatus().toString()) + triple -> { + TicketMetadata metadata = null; + if(triple.getRight() != null) { + metadata = new TicketMetadata(null, null, triple.getRight()); + } + return new MapSqlParameterSource("reservationId", reservationId) + .addValue("ticketId", triple.getLeft()) + .addValue("specialCodeId", triple.getMiddle().getId()) + .addValue("userLanguage", locale.getLanguage()) + .addValue("srcPriceCts", category.getSrcPriceCts()) + .addValue("currencyCode", category.getCurrencyCode()) + .addValue("ticketMetadata", json.asJsonString(metadata)) + .addValue("vatStatus", event.getVatStatus().toString()); + } ).toArray(MapSqlParameterSource[]::new)); specialPriceRepository.batchUpdateStatus( specialPrices.stream().map(SpecialPrice::getId).collect(toList()), @@ -482,13 +501,20 @@ void reserveTicketsForCategory(Event event, priceContainer.getVatStatus()); } + private static T getAtIndexOrNull(List elements, int index) { + if (elements == null || index >= elements.size()) { + return null; + } + return elements.get(index); + } + List reserveTokensForAccessCode(TicketReservationWithOptionalCodeModification ticketReservation, PromoCodeDiscount accessCode) { try { // since we're going to get some tokens for an access code, we lock the access code itself until we're done. // This will allow us to serialize the requests and limit the contention Validate.isTrue(promoCodeDiscountRepository.lockAccessCodeForUpdate(accessCode.getId()).equals(accessCode.getId())); - List boundSpecialPrices = specialPriceRepository.bindToAccessCode(ticketReservation.getTicketCategoryId(), accessCode.getId(), ticketReservation.getAmount()); - if(boundSpecialPrices.size() != ticketReservation.getAmount()) { + List boundSpecialPrices = specialPriceRepository.bindToAccessCode(ticketReservation.getTicketCategoryId(), accessCode.getId(), ticketReservation.getQuantity()); + if(boundSpecialPrices.size() != ticketReservation.getQuantity()) { throw new NotEnoughTicketsException(); } return boundSpecialPrices; @@ -530,7 +556,7 @@ private void reserveAdditionalServicesForReservation(int eventId, String transac } List reserveTickets(int eventId, TicketReservationWithOptionalCodeModification ticketReservation, List requiredStatuses) { - return reserveTickets(eventId, ticketReservation.getTicketCategoryId(), ticketReservation.getAmount(), requiredStatuses); + return reserveTickets(eventId, ticketReservation.getTicketCategoryId(), ticketReservation.getQuantity(), requiredStatuses); } List reserveTickets(int eventId , int categoryId, int qty, List requiredStatuses) { @@ -561,7 +587,7 @@ Optional fixToken(Optional token, int ticketCategory && specialPrice.get().getTicketCategoryId() == ticketCategoryId; - if (canAccessRestrictedCategory && ticketReservation.getAmount() > 1) { + if (canAccessRestrictedCategory && ticketReservation.getQuantity() > 1) { throw new NotEnoughTicketsException(); } diff --git a/src/main/java/alfio/manager/WaitingQueueManager.java b/src/main/java/alfio/manager/WaitingQueueManager.java index e64ecb2767..5d58752e29 100644 --- a/src/main/java/alfio/manager/WaitingQueueManager.java +++ b/src/main/java/alfio/manager/WaitingQueueManager.java @@ -229,7 +229,7 @@ private Stream Pair.of(wq, tickets.next())) .map(pair -> { TicketReservationModification ticketReservation = new TicketReservationModification(); - ticketReservation.setAmount(1); + ticketReservation.setQuantity(1); Integer categoryId = Optional.ofNullable(pair.getValue().getCategoryId()).orElseGet(() -> findBestCategory(unboundedCategories, pair.getKey()).orElseThrow(RuntimeException::new).getId()); ticketReservation.setTicketCategoryId(categoryId); return Pair.of(pair.getLeft(), new TicketReservationWithOptionalCodeModification(ticketReservation, Optional.empty())); diff --git a/src/main/java/alfio/model/api/v1/admin/ReservationCreationRequest.java b/src/main/java/alfio/model/api/v1/admin/ReservationCreationRequest.java new file mode 100644 index 0000000000..4996ff4bbf --- /dev/null +++ b/src/main/java/alfio/model/api/v1/admin/ReservationCreationRequest.java @@ -0,0 +1,76 @@ +/** + * 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 . + */ +package alfio.model.api.v1.admin; + +import alfio.controller.form.ReservationCreate; +import alfio.model.modification.AdditionalServiceReservationModification; +import alfio.model.modification.TicketReservationModification; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class ReservationCreationRequest implements ReservationCreate { + + private final List tickets; + private final List additionalServices; + private final ReservationUser user; + private final String promoCode; + private final String language; + + @JsonCreator + public ReservationCreationRequest(@JsonProperty("tickets") List tickets, + @JsonProperty("additionalServices") List additionalServices, + @JsonProperty("user") ReservationUser user, + @JsonProperty("promoCode") String promoCode, + @JsonProperty("language") String language) { + this.tickets = tickets; + this.additionalServices = additionalServices; + this.user = user; + this.promoCode = promoCode; + this.language = language; + } + + + @Override + public String getPromoCode() { + return promoCode; + } + + @Override + public List getTickets() { + return tickets; + } + + @Override + public List getAdditionalServices() { + return additionalServices; + } + + @Override + public String getCaptcha() { + return null; + } + + public String getLanguage() { + return language; + } + + public ReservationUser getUser() { + return user; + } +} diff --git a/src/main/java/alfio/model/api/v1/admin/ReservationUser.java b/src/main/java/alfio/model/api/v1/admin/ReservationUser.java new file mode 100644 index 0000000000..2c772b7ddc --- /dev/null +++ b/src/main/java/alfio/model/api/v1/admin/ReservationUser.java @@ -0,0 +1,61 @@ +/** + * 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 . + */ +package alfio.model.api.v1.admin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ReservationUser { + private final String username; + private final String firstName; + private final String lastName; + private final String email; + private final String id; + + @JsonCreator + ReservationUser(@JsonProperty("username") String username, + @JsonProperty("firstName") String firstName, + @JsonProperty("lastName") String lastName, + @JsonProperty("email") String email, + @JsonProperty("id") String id) { + this.username = username; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.id = id; + } + + public String getUsername() { + return username; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getEmail() { + return email; + } + + public String getId() { + return id; + } +} diff --git a/src/main/java/alfio/model/modification/TicketReservationModification.java b/src/main/java/alfio/model/modification/TicketReservationModification.java index bc4430d0c8..be97217981 100644 --- a/src/main/java/alfio/model/modification/TicketReservationModification.java +++ b/src/main/java/alfio/model/modification/TicketReservationModification.java @@ -19,9 +19,23 @@ import lombok.Data; import java.io.Serializable; +import java.util.List; +import java.util.Map; @Data public class TicketReservationModification implements Serializable { private Integer ticketCategoryId; - private Integer amount; + private Integer quantity; + private List> metadata; + + // temporary until we replace the public front-end + @Deprecated + public Integer getAmount() { + return quantity; + } + + @Deprecated + public void setAmount(Integer amount) { + this.quantity = amount; + } } diff --git a/src/main/java/alfio/repository/TicketRepository.java b/src/main/java/alfio/repository/TicketRepository.java index 3013e0044e..f1486b9458 100644 --- a/src/main/java/alfio/repository/TicketRepository.java +++ b/src/main/java/alfio/repository/TicketRepository.java @@ -18,6 +18,7 @@ import alfio.model.*; import alfio.model.checkin.OnlineCheckInFullInfo; +import alfio.model.metadata.TicketMetadata; import alfio.model.metadata.TicketMetadataContainer; import alfio.model.poll.PollParticipant; import alfio.model.support.Array; @@ -112,19 +113,20 @@ default void bulkTicketUpdate(List ids, TicketCategory ticketCategory) @Query(type = QueryType.TEMPLATE, value = "update ticket set tickets_reservation_id = :reservationId, special_price_id_fk = :specialCodeId," + " user_language = :userLanguage, status = 'PENDING', src_price_cts = :srcPriceCts," + - " currency_code = :currencyCode, vat_status = :vatStatus::VAT_STATUS where id = :ticketId") + " currency_code = :currencyCode, vat_status = :vatStatus::VAT_STATUS, metadata = :ticketMetadata::jsonb where id = :ticketId") String batchReserveTicket(); @Query("update ticket set tickets_reservation_id = :reservationId, special_price_id_fk = :specialCodeId," + " user_language = :userLanguage, status = 'PENDING', src_price_cts = :srcPriceCts, currency_code = :currencyCode," + - " vat_status = :vatStatus::VAT_STATUS where id = :ticketId") + " vat_status = :vatStatus::VAT_STATUS, metadata = :ticketMetadata::jsonb where id = :ticketId") void reserveTicket(@Bind("reservationId")String transactionId, @Bind("ticketId") int ticketId, @Bind("specialCodeId") int specialCodeId, @Bind("userLanguage") String userLanguage, @Bind("srcPriceCts") int srcPriceCts, @Bind("currencyCode") String currencyCode, - @Bind("vatStatus") @EnumTypeAsString PriceContainer.VatStatus vatStatus); + @Bind("vatStatus") @EnumTypeAsString PriceContainer.VatStatus vatStatus, + @Bind("ticketMetadata") @JSONData TicketMetadata ticketMetadata); @Query("update ticket set status = :status where tickets_reservation_id = :reservationId") int updateTicketsStatusWithReservationId(@Bind("reservationId") String reservationId, @Bind("status") String status); diff --git a/src/main/java/alfio/util/ReservationUtil.java b/src/main/java/alfio/util/ReservationUtil.java new file mode 100644 index 0000000000..5337971011 --- /dev/null +++ b/src/main/java/alfio/util/ReservationUtil.java @@ -0,0 +1,166 @@ +/** + * 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 . + */ +package alfio.util; + +import alfio.controller.decorator.SaleableTicketCategory; +import alfio.controller.form.ReservationCreate; +import alfio.manager.EventManager; +import alfio.manager.PromoCodeRequestManager; +import alfio.manager.TicketReservationManager; +import alfio.manager.support.response.ValidatedResponse; +import alfio.model.*; +import alfio.model.modification.ASReservationWithOptionalCodeModification; +import alfio.model.modification.AdditionalServiceReservationModification; +import alfio.model.modification.TicketReservationModification; +import alfio.model.modification.TicketReservationWithOptionalCodeModification; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; + +public class ReservationUtil { + + private ReservationUtil() { + } + + public static Optional checkPromoCode(ReservationCreate createRequest, + Event event, + PromoCodeRequestManager promoCodeRequestManager, + BindingResult bindingResult) { + Optional, Optional>>> codeCheck = Optional.empty(); + + if(StringUtils.trimToNull(createRequest.getPromoCode()) != null) { + var resCheck = promoCodeRequestManager.checkCode(event, createRequest.getPromoCode()); + if(!resCheck.isSuccess()) { + bindingResult.reject(ErrorsCode.STEP_1_CODE_NOT_FOUND, ErrorsCode.STEP_1_CODE_NOT_FOUND); + } + codeCheck = Optional.of(resCheck); + } + + return codeCheck.map(ValidatedResponse::getValue) + .flatMap(Pair::getRight) + .map(PromoCodeDiscount::getPromoCode); + } + + + public static Optional, List>> validateCreateRequest(ReservationCreate request, + Errors bindingResult, + TicketReservationManager tickReservationManager, + EventManager eventManager, + String validatedPromoCodeDiscount, + Event event) { + + + + int selectionCount = ticketSelectionCount(request.getTickets()); + + if (selectionCount <= 0) { + bindingResult.reject(ErrorsCode.STEP_1_SELECT_AT_LEAST_ONE); + return Optional.empty(); + } + + List> maxTicketsByTicketReservation = selected(request.getTickets()).stream() + .map(r -> Pair.of(r, tickReservationManager.maxAmountOfTicketsForCategory(event, r.getTicketCategoryId(), validatedPromoCodeDiscount))) + .collect(toList()); + Optional> error = maxTicketsByTicketReservation.stream() + .filter(p -> p.getKey().getQuantity() > p.getValue()) + .findAny(); + + if(error.isPresent()) { + bindingResult.reject(ErrorsCode.STEP_1_OVER_MAXIMUM, new Object[] { error.get().getValue() }, null); + return Optional.empty(); + } + + final List categories = selected(request.getTickets()); + final List additionalServices = selectedAdditionalServices(request.getAdditionalServices()); + + final boolean validCategorySelection = categories.stream().allMatch(c -> { + TicketCategory tc = eventManager.getTicketCategoryById(c.getTicketCategoryId(), event.getId()); + return eventManager.eventExistsById(tc.getEventId()); + }); + + + final boolean validAdditionalServiceSelected = additionalServices.stream().allMatch(asm -> { + AdditionalService as = eventManager.getAdditionalServiceById(asm.getAdditionalServiceId(), event.getId()); + ZonedDateTime now = event.now(ClockProvider.clock()); + return as.getInception(event.getZoneId()).isBefore(now) && + as.getExpiration(event.getZoneId()).isAfter(now) && + asm.getQuantity() >= 0 && + ((as.isFixPrice() && asm.isQuantityValid(as, selectionCount)) || (!as.isFixPrice() && asm.getAmount() != null && asm.getAmount().compareTo(BigDecimal.ZERO) >= 0)) && + eventManager.eventExistsById(as.getEventId()); + }); + + if(!validCategorySelection || !validAdditionalServiceSelected) { + bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE); + return Optional.empty(); + } + + List res = new ArrayList<>(); + // + Optional specialCode = Optional.ofNullable(StringUtils.trimToNull(request.getPromoCode())) + .flatMap(tickReservationManager::getSpecialPriceByCode); + // + final ZonedDateTime now = event.now(ClockProvider.clock()); + maxTicketsByTicketReservation.forEach(pair -> validateCategory(bindingResult, tickReservationManager, eventManager, event, pair.getRight(), res, specialCode, now, pair.getLeft())); + return bindingResult.hasErrors() ? Optional.empty() : Optional.of(Pair.of(res, additionalServices.stream().map(as -> new ASReservationWithOptionalCodeModification(as, specialCode)).collect(Collectors.toList()))); + } + + private static int ticketSelectionCount(List tickets) { + return selected(tickets).stream().mapToInt(TicketReservationModification::getQuantity).sum(); + } + + private static void validateCategory(Errors bindingResult, TicketReservationManager tickReservationManager, EventManager eventManager, + Event event, int maxAmountOfTicket, List res, + Optional specialCode, ZonedDateTime now, TicketReservationModification r) { + TicketCategory tc = eventManager.getTicketCategoryById(r.getTicketCategoryId(), event.getId()); + SaleableTicketCategory ticketCategory = new SaleableTicketCategory(tc, now, event, tickReservationManager.countAvailableTickets(event, tc), maxAmountOfTicket, null); + + if (!ticketCategory.getSaleable()) { + bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE); + return; + } + + res.add(new TicketReservationWithOptionalCodeModification(r, ticketCategory.isAccessRestricted() ? specialCode : Optional.empty())); + } + + private static List selected(List reservation) { + return ofNullable(reservation) + .orElse(emptyList()) + .stream() + .filter(e -> e != null && e.getQuantity() != null && e.getTicketCategoryId() != null && e.getQuantity() > 0) + .collect(toList()); + } + + private static List selectedAdditionalServices(List additionalServices) { + return ofNullable(additionalServices) + .orElse(emptyList()) + .stream() + .filter(e -> e != null && e.getQuantity() != null && e.getAdditionalServiceId() != null && e.getQuantity() > 0) + .collect(toList()); + } +} diff --git a/src/test/java/alfio/manager/TicketReservationManagerTest.java b/src/test/java/alfio/manager/TicketReservationManagerTest.java index 47ba47c0b9..f3f83c2476 100644 --- a/src/test/java/alfio/manager/TicketReservationManagerTest.java +++ b/src/test/java/alfio/manager/TicketReservationManagerTest.java @@ -565,7 +565,7 @@ void reserveTicketsForCategoryWithAccessCode() { PromoCodeDiscount discount = mock(PromoCodeDiscount.class); when(discount.getCodeType()).thenReturn(PromoCodeDiscount.CodeType.ACCESS); when(reservationModification.getTicketCategoryId()).thenReturn(TICKET_CATEGORY_ID); - when(reservationModification.getAmount()).thenReturn(2); + when(reservationModification.getQuantity()).thenReturn(2); when(discount.getHiddenCategoryId()).thenReturn(TICKET_CATEGORY_ID); int accessCodeId = 666; when(discount.getId()).thenReturn(accessCodeId); @@ -594,7 +594,7 @@ void reserveTicketsForBoundedCategories() { when(ticketCategory.isBounded()).thenReturn(true); List ids = singletonList(1); when(ticketRepository.selectTicketInCategoryForUpdateSkipLocked(eq(EVENT_ID), eq(TICKET_CATEGORY_ID), eq(1), eq(singletonList(Ticket.TicketStatus.FREE.name())))).thenReturn(ids); - when(reservationModification.getAmount()).thenReturn(1); + when(reservationModification.getQuantity()).thenReturn(1); when(reservationModification.getTicketCategoryId()).thenReturn(TICKET_CATEGORY_ID); when(ticketRepository.findById(1, TICKET_CATEGORY_ID)).thenReturn(ticket); trm.reserveTicketsForCategory(event, "trid", reservationModification, Locale.ENGLISH, false, null, null); @@ -606,7 +606,7 @@ void reserveTicketsForBoundedCategoriesWaitingQueue() { when(ticketCategory.isBounded()).thenReturn(true); List ids = singletonList(1); when(ticketRepository.selectTicketInCategoryForUpdateSkipLocked(eq(EVENT_ID), eq(TICKET_CATEGORY_ID), eq(1), eq(asList(TicketStatus.RELEASED.name(), TicketStatus.PRE_RESERVED.name())))).thenReturn(ids); - when(reservationModification.getAmount()).thenReturn(1); + when(reservationModification.getQuantity()).thenReturn(1); when(reservationModification.getTicketCategoryId()).thenReturn(TICKET_CATEGORY_ID); when(ticketRepository.findById(1, TICKET_CATEGORY_ID)).thenReturn(ticket); trm.reserveTicketsForCategory(event, "trid", reservationModification, Locale.ENGLISH, true, null, null); @@ -618,7 +618,7 @@ void reserveTicketsForUnboundedCategories() { when(ticketCategory.isBounded()).thenReturn(false); List ids = singletonList(1); when(ticketRepository.selectNotAllocatedTicketsForUpdateSkipLocked(eq(EVENT_ID), eq(1), eq(singletonList(Ticket.TicketStatus.FREE.name())))).thenReturn(ids); - when(reservationModification.getAmount()).thenReturn(1); + when(reservationModification.getQuantity()).thenReturn(1); when(reservationModification.getTicketCategoryId()).thenReturn(TICKET_CATEGORY_ID); when(ticketRepository.findById(1, TICKET_CATEGORY_ID)).thenReturn(ticket); trm.reserveTicketsForCategory(event, "trid", reservationModification, Locale.ENGLISH, false, null, null); @@ -630,7 +630,7 @@ void reserveTicketsForUnboundedCategoriesWaitingQueue() { when(ticketCategory.isBounded()).thenReturn(false); List ids = singletonList(1); when(ticketRepository.selectNotAllocatedTicketsForUpdateSkipLocked(eq(EVENT_ID), eq(1), eq(asList(TicketStatus.RELEASED.name(), TicketStatus.PRE_RESERVED.name())))).thenReturn(ids); - when(reservationModification.getAmount()).thenReturn(1); + when(reservationModification.getQuantity()).thenReturn(1); when(reservationModification.getTicketCategoryId()).thenReturn(TICKET_CATEGORY_ID); when(ticketRepository.findById(1, TICKET_CATEGORY_ID)).thenReturn(ticket); trm.reserveTicketsForCategory(event, "trid", reservationModification, Locale.ENGLISH, true, null, null); From 9da8a0ee36daa1b445f70d8881c15058f1d8e5bf Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Fri, 5 Nov 2021 12:10:02 +0100 Subject: [PATCH 2/4] create user and trigger authentication on redirect if specified --- .../v1/admin/ReservationApiV1Controller.java | 21 ++++++++++++-- .../manager/TicketReservationManager.java | 28 ++++++++++++++++++- .../java/alfio/manager/user/UserManager.java | 19 +++++++++++++ .../alfio/repository/user/UserRepository.java | 10 +++++++ .../manager/TicketReservationManagerTest.java | 27 ++++++++++++++++++ 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java index b2bfc5436c..e92157e0c8 100644 --- a/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java +++ b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java @@ -20,11 +20,15 @@ import alfio.manager.PromoCodeRequestManager; import alfio.manager.TicketReservationManager; import alfio.manager.system.ConfigurationManager; +import alfio.manager.user.UserManager; import alfio.model.api.v1.admin.ReservationCreationRequest; import alfio.model.result.ErrorCode; import alfio.util.ReservationUtil; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.bind.annotation.*; @@ -44,16 +48,19 @@ public class ReservationApiV1Controller { private final ConfigurationManager configurationManager; private final EventManager eventManager; private final PromoCodeRequestManager promoCodeRequestManager; + private final UserManager userManager; @Autowired public ReservationApiV1Controller(TicketReservationManager ticketReservationManager, ConfigurationManager configurationManager, EventManager eventManager, - PromoCodeRequestManager promoCodeRequestManager) { + PromoCodeRequestManager promoCodeRequestManager, + UserManager userManager) { this.ticketReservationManager = ticketReservationManager; this.configurationManager = configurationManager; this.eventManager = eventManager; this.promoCodeRequestManager = promoCodeRequestManager; + this.userManager = userManager; } @PostMapping("/{slug}/reservation") @@ -73,7 +80,13 @@ public ResponseEntity createReservation(@PathVariable("slug") if(selected.isPresent() && !bindingResult.hasErrors()) { var pair = selected.get(); return ticketReservationManager.createTicketReservation(event, pair.getLeft(), pair.getRight(), promoCodeDiscount, locale, bindingResult, principal) - .map(id -> ResponseEntity.ok(CreationResponse.success(id, ticketReservationManager.reservationUrl(id, event)))) + .map(id -> { + var user = reservationCreationRequest.getUser(); + if(user != null) { + ticketReservationManager.setReservationOwner(id, user.getEmail(), user.getFirstName(), user.getLastName()); + } + return ResponseEntity.ok(CreationResponse.success(id, ticketReservationManager.reservationUrlForExternalClients(id, event, locale.getLanguage(), user != null))); + }) .orElseGet(() -> ResponseEntity.badRequest().build()); } else { return ResponseEntity.badRequest() @@ -104,6 +117,10 @@ public List getErrors() { return errors; } + public boolean isSuccess() { + return CollectionUtils.isEmpty(errors) && StringUtils.isNotEmpty(id); + } + static CreationResponse success(String id, String href) { return new CreationResponse(id, href, null); } diff --git a/src/main/java/alfio/manager/TicketReservationManager.java b/src/main/java/alfio/manager/TicketReservationManager.java index 2b8a02da26..e6ea2d5c81 100644 --- a/src/main/java/alfio/manager/TicketReservationManager.java +++ b/src/main/java/alfio/manager/TicketReservationManager.java @@ -1806,9 +1806,23 @@ String reservationUrl(String reservationId) { public String reservationUrl(String reservationId, PurchaseContext purchaseContext) { return reservationUrl(ticketReservationRepository.findReservationById(reservationId), purchaseContext); } + + public String reservationUrlForExternalClients(String reservationId, PurchaseContext purchaseContext, String userLanguage, boolean userLoggedIn) { + var configMap = configurationManager.getFor(EnumSet.of(BASE_URL, OPENID_PUBLIC_ENABLED), purchaseContext.getConfigurationLevel()); + var baseUrl = StringUtils.removeEnd(configMap.get(BASE_URL).getRequiredValue(), "/"); + if(userLoggedIn && configMap.get(OPENID_PUBLIC_ENABLED).getValueAsBooleanOrDefault()) { + return baseUrl + "/openid/authentication?reservation=" + reservationId + "&contextType=" + purchaseContext.getType() + "&id=" + purchaseContext.getPublicIdentifier(); + } else { + return reservationUrl(baseUrl, reservationId, purchaseContext, userLanguage); + } + } + + String reservationUrl(String baseUrl, String reservationId, PurchaseContext purchaseContext, String userLanguage) { + return StringUtils.removeEnd(baseUrl, "/") + "/" + purchaseContext.getType()+ "/" + purchaseContext.getPublicIdentifier() + "/reservation/" + reservationId + "?lang="+userLanguage; + } String reservationUrl(TicketReservation reservation, PurchaseContext purchaseContext) { - return configurationManager.baseUrl(purchaseContext) + "/"+purchaseContext.getType()+ "/" + purchaseContext.getPublicIdentifier() + "/reservation/" + reservation.getId() + "?lang="+reservation.getUserLanguage(); + return reservationUrl(configurationManager.baseUrl(purchaseContext), reservation.getId(), purchaseContext, reservation.getUserLanguage()); } String ticketUrl(Event event, String ticketId) { @@ -2367,6 +2381,18 @@ public void updateReservation(String reservationId, CustomerName customerName, S } } + public void setReservationOwner(String reservationId, String email, String firstName, String lastName) { + // make sure that user has been created + var userId = userManager.createPublicUserIfNotExists(email, firstName, lastName); + // assign reservation to user + if(userId != null) { + ticketReservationRepository.setReservationOwner(reservationId, userId); + log.info("Assigned reservation {} to user ID {}", reservationId, userId); + } else { + log.info("UserId not found. Leaving reservation {} public", reservationId); + } + } + static String buildCompleteBillingAddress(CustomerName customerName, String billingAddressCompany, String billingAddressLine1, diff --git a/src/main/java/alfio/manager/user/UserManager.java b/src/main/java/alfio/manager/user/UserManager.java index cd7711a707..a2517e7900 100644 --- a/src/main/java/alfio/manager/user/UserManager.java +++ b/src/main/java/alfio/manager/user/UserManager.java @@ -30,6 +30,7 @@ import alfio.util.RequestUtils; import ch.digitalfondue.npjt.AffectedRowCountAndKey; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.Pair; @@ -47,11 +48,13 @@ import java.util.stream.Stream; import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; import static java.util.stream.Collectors.toList; @Component @Transactional @RequiredArgsConstructor +@Log4j2 public class UserManager { public static final String ADMIN_USERNAME = "admin"; @@ -327,4 +330,20 @@ public ValidationResult validateNewPassword(String username, String oldPassword, .orElseGet(ValidationResult::failed); } + public Integer createPublicUserIfNotExists(String email, String firstName, String lastName) { + var result = userRepository.createPublicUserIfNotExists(email, + passwordEncoder.encode(PasswordGenerator.generateRandomPassword()), + firstName, + lastName, + email, + true); + if (result.getAffectedRowCount() == 1) { + log.info("Created public user with id {}", result.getKey()); + return result.getKey(); + } else { + log.info("User was not created because already existed"); + return userRepository.findIdByUserName(email).orElse(null); + } + } + } diff --git a/src/main/java/alfio/repository/user/UserRepository.java b/src/main/java/alfio/repository/user/UserRepository.java index adf24fb11a..4d111c5d3a 100644 --- a/src/main/java/alfio/repository/user/UserRepository.java +++ b/src/main/java/alfio/repository/user/UserRepository.java @@ -75,6 +75,16 @@ AffectedRowCountAndKey create(@Bind("username") String username, @Bind( @Bind("email_address") String emailAddress, @Bind("enabled") boolean enabled, @Bind("userType") User.Type userType, @Bind("validTo") ZonedDateTime validTo, @Bind("description") String description); + @Query("INSERT INTO ba_user(username, password, first_name, last_name, email_address, enabled, user_type) VALUES" + + " (:username, :password, :first_name, :last_name, :email_address, :enabled, 'PUBLIC') on conflict(username) do nothing") + @AutoGeneratedKey("id") + AffectedRowCountAndKey createPublicUserIfNotExists(@Bind("username") String username, + @Bind("password") String password, + @Bind("first_name") String firstname, + @Bind("last_name") String lastname, + @Bind("email_address") String emailAddress, + @Bind("enabled") boolean enabled); + @Query("update ba_user set username = :username, first_name = :firstName, last_name = :lastName, email_address = :emailAddress, description = :description where id = :id") int update(@Bind("id") int id, @Bind("username") String username, @Bind("firstName") String firstName, @Bind("lastName") String lastName, @Bind("emailAddress") String emailAddress, @Bind("description") String description); diff --git a/src/test/java/alfio/manager/TicketReservationManagerTest.java b/src/test/java/alfio/manager/TicketReservationManagerTest.java index f3f83c2476..97dc75f762 100644 --- a/src/test/java/alfio/manager/TicketReservationManagerTest.java +++ b/src/test/java/alfio/manager/TicketReservationManagerTest.java @@ -71,6 +71,7 @@ import static alfio.model.Audit.EventType.PAYMENT_CONFIRMED; import static alfio.model.TicketReservation.TicketReservationStatus.*; import static alfio.model.system.ConfigurationKeys.*; +import static alfio.model.system.ConfigurationKeys.BASE_URL; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; @@ -1115,6 +1116,32 @@ void reservationURLGeneration() { Assertions.assertEquals(BASE_URL + "event/" + shortName + "/ticket/ticketId/update?lang=it", trm.ticketUpdateUrl(event, "ticketId")); } + @Test + void reservationUrlForExternalClients() { + String shortName = "shortName"; + when(event.getType()).thenReturn(PurchaseContext.PurchaseContextType.event); + when(event.getPublicIdentifier()).thenReturn(shortName); + when(ticketReservation.getUserLanguage()).thenReturn("en"); + when(ticketReservation.getId()).thenReturn(RESERVATION_ID); + + var maybeOpenId = mock(MaybeConfiguration.class); + var maybeBaseUrl = mock(MaybeConfiguration.class); + when(maybeBaseUrl.getRequiredValue()).thenReturn(BASE_URL); + + when(configurationManager.getFor(eq(EnumSet.of(ConfigurationKeys.BASE_URL, OPENID_PUBLIC_ENABLED)), any())) + .thenReturn(Map.of(ConfigurationKeys.BASE_URL, maybeBaseUrl, OPENID_PUBLIC_ENABLED, maybeOpenId)); + + // OpenID active + when(maybeOpenId.getValueAsBooleanOrDefault()).thenReturn(true); + Assertions.assertEquals(BASE_URL + "openid/authentication?reservation=" + RESERVATION_ID + "&contextType=" + PurchaseContext.PurchaseContextType.event + "&id=" + shortName, trm.reservationUrlForExternalClients(RESERVATION_ID, event, "en", true)); + + // user not specified in the request + Assertions.assertEquals(BASE_URL + "event/" + shortName + "/reservation/" + RESERVATION_ID + "?lang=en", trm.reservationUrlForExternalClients(RESERVATION_ID, event, "en", false)); + // OpenID not active + when(maybeOpenId.getValueAsBooleanOrDefault()).thenReturn(false); + Assertions.assertEquals(BASE_URL + "event/" + shortName + "/reservation/" + RESERVATION_ID + "?lang=en", trm.reservationUrlForExternalClients(RESERVATION_ID, event, "en", true)); + } + //sendReminderForOptionalInfo private void initReminder() { when(ticketFieldRepository.countAdditionalFieldsForEvent(EVENT_ID)).thenReturn(1); From 7fdc405aeded099efa4f94c7af1d30858acc9784 Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Fri, 5 Nov 2021 13:10:29 +0100 Subject: [PATCH 3/4] set user as reservation contact --- .../v1/admin/ReservationApiV1Controller.java | 2 +- .../manager/TicketReservationManager.java | 41 +++++++++++++++---- .../java/alfio/manager/user/UserManager.java | 11 +++-- .../TicketReservationRepository.java | 2 +- .../alfio/repository/user/UserRepository.java | 3 +- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java index e92157e0c8..75f90e68c3 100644 --- a/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java +++ b/src/main/java/alfio/controller/api/v1/admin/ReservationApiV1Controller.java @@ -83,7 +83,7 @@ public ResponseEntity createReservation(@PathVariable("slug") .map(id -> { var user = reservationCreationRequest.getUser(); if(user != null) { - ticketReservationManager.setReservationOwner(id, user.getEmail(), user.getFirstName(), user.getLastName()); + ticketReservationManager.setReservationOwner(id, user.getUsername(), user.getEmail(), user.getFirstName(), user.getLastName(), locale.getLanguage()); } return ResponseEntity.ok(CreationResponse.success(id, ticketReservationManager.reservationUrlForExternalClients(id, event, locale.getLanguage(), user != null))); }) diff --git a/src/main/java/alfio/manager/TicketReservationManager.java b/src/main/java/alfio/manager/TicketReservationManager.java index e6ea2d5c81..89cb94930a 100644 --- a/src/main/java/alfio/manager/TicketReservationManager.java +++ b/src/main/java/alfio/manager/TicketReservationManager.java @@ -2381,16 +2381,39 @@ public void updateReservation(String reservationId, CustomerName customerName, S } } - public void setReservationOwner(String reservationId, String email, String firstName, String lastName) { - // make sure that user has been created - var userId = userManager.createPublicUserIfNotExists(email, firstName, lastName); - // assign reservation to user - if(userId != null) { - ticketReservationRepository.setReservationOwner(reservationId, userId); - log.info("Assigned reservation {} to user ID {}", reservationId, userId); + public void setReservationOwner(String reservationId, + String username, + String email, + String firstName, + String lastName, + String userLanguage) { + if(configurationManager.isPublicOpenIdEnabled()) { + // make sure that user has been created + var userId = userManager.createPublicUserIfNotExists(username, email, firstName, lastName); + // assign reservation to user + if (userId != null) { + ticketReservationRepository.setReservationOwner(reservationId, userId); + log.info("Assigned reservation {} to user ID {}", reservationId, userId); + } else { + log.info("UserId not found. Leaving reservation {} anonymous", reservationId); + } } else { - log.info("UserId not found. Leaving reservation {} public", reservationId); - } + log.info("Public OpenID is not enabled. Leaving reservation {} anonymous", reservationId); + } + // in any case we can safely set the given user as contact + var customerName = new CustomerName(null, firstName, lastName, true); + ticketReservationRepository.updateTicketReservation(reservationId, + Status.PENDING.toString(), + email, + customerName.getFullName(), + customerName.getFirstName(), + customerName.getLastName(), + userLanguage, + null, + null, + null, + null + ); } static String buildCompleteBillingAddress(CustomerName customerName, diff --git a/src/main/java/alfio/manager/user/UserManager.java b/src/main/java/alfio/manager/user/UserManager.java index a2517e7900..35ce7ac918 100644 --- a/src/main/java/alfio/manager/user/UserManager.java +++ b/src/main/java/alfio/manager/user/UserManager.java @@ -330,20 +330,19 @@ public ValidationResult validateNewPassword(String username, String oldPassword, .orElseGet(ValidationResult::failed); } - public Integer createPublicUserIfNotExists(String email, String firstName, String lastName) { - var result = userRepository.createPublicUserIfNotExists(email, + public Integer createPublicUserIfNotExists(String username, String email, String firstName, String lastName) { + int result = userRepository.createPublicUserIfNotExists(username, passwordEncoder.encode(PasswordGenerator.generateRandomPassword()), firstName, lastName, email, true); - if (result.getAffectedRowCount() == 1) { - log.info("Created public user with id {}", result.getKey()); - return result.getKey(); + if (result == 1) { + log.info("Created public user"); } else { log.info("User was not created because already existed"); - return userRepository.findIdByUserName(email).orElse(null); } + return userRepository.findIdByUserName(username).orElse(null); } } diff --git a/src/main/java/alfio/repository/TicketReservationRepository.java b/src/main/java/alfio/repository/TicketReservationRepository.java index cd9824b06e..f58aca8657 100644 --- a/src/main/java/alfio/repository/TicketReservationRepository.java +++ b/src/main/java/alfio/repository/TicketReservationRepository.java @@ -61,7 +61,7 @@ int updateTicketReservation(@Bind("reservationId") String reservationId, @Bind("lastName") String lastName, @Bind("userLanguage") String userLanguage, @Bind("billingAddress") String billingAddress, - @Bind("timestamp") ZonedDateTime timestamp, + @Bind("timestamp") ZonedDateTime confirmationTimestamp, @Bind("paymentMethod") String paymentMethod, @Bind("customerReference") String customerReference); diff --git a/src/main/java/alfio/repository/user/UserRepository.java b/src/main/java/alfio/repository/user/UserRepository.java index 4d111c5d3a..1f5bf51955 100644 --- a/src/main/java/alfio/repository/user/UserRepository.java +++ b/src/main/java/alfio/repository/user/UserRepository.java @@ -77,8 +77,7 @@ AffectedRowCountAndKey create(@Bind("username") String username, @Bind( @Query("INSERT INTO ba_user(username, password, first_name, last_name, email_address, enabled, user_type) VALUES" + " (:username, :password, :first_name, :last_name, :email_address, :enabled, 'PUBLIC') on conflict(username) do nothing") - @AutoGeneratedKey("id") - AffectedRowCountAndKey createPublicUserIfNotExists(@Bind("username") String username, + int createPublicUserIfNotExists(@Bind("username") String username, @Bind("password") String password, @Bind("first_name") String firstname, @Bind("last_name") String lastname, From 9e2bdbd52428c9ebe89b4515f42117c29d3e835b Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Fri, 5 Nov 2021 17:12:57 +0100 Subject: [PATCH 4/4] #1031 allow the reservation process to be opened in an iframe for specific ancestors --- .../AbstractFormBasedWebSecurity.java | 2 + .../alfio/controller/IndexController.java | 49 +++++++++++++------ .../alfio/manager/PurchaseContextManager.java | 23 +++++++++ .../alfio/model/system/ConfigurationKeys.java | 2 + .../repository/SubscriptionRepository.java | 3 ++ 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java b/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java index 4f8bf6cc21..e0cd288a29 100644 --- a/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java +++ b/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java @@ -130,6 +130,8 @@ protected void configure(HttpSecurity http) throws Exception { }; configurer.csrfTokenRepository(csrfTokenRepository) + .and() + .headers().frameOptions().disable() // https://github.com/alfio-event/alf.io/issues/1031 X-Frame-Options has been moved to IndexController .and() .authorizeRequests() .antMatchers(HttpMethod.GET, ADMIN_API + "/users/current").hasAnyRole(ADMIN, OWNER, SUPERVISOR) diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index 12c4c07e24..9b0605804f 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -20,14 +20,12 @@ import alfio.config.WebSecurityConfig; import alfio.config.authentication.support.OpenIdAlfioAuthentication; import alfio.controller.api.v2.user.support.EventLoader; +import alfio.manager.PurchaseContextManager; import alfio.manager.i18n.MessageSourceManager; import alfio.manager.openid.OpenIdAuthenticationManager; import alfio.manager.system.ConfigurationLevel; import alfio.manager.system.ConfigurationManager; -import alfio.model.ContentLanguage; -import alfio.model.EventDescription; -import alfio.model.FileBlobMetadata; -import alfio.model.TicketReservationStatusAndValidation; +import alfio.model.*; import alfio.model.system.ConfigurationKeys; import alfio.model.user.Role; import alfio.repository.*; @@ -39,9 +37,8 @@ import ch.digitalfondue.jfiveparse.*; import lombok.AllArgsConstructor; import org.apache.commons.codec.binary.Hex; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IterableUtils; -import org.apache.commons.collections4.IteratorUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; @@ -108,6 +105,7 @@ public class IndexController { private final TicketReservationRepository ticketReservationRepository; private final SubscriptionRepository subscriptionRepository; private final EventLoader eventLoader; + private final PurchaseContextManager purchaseContextManager; @RequestMapping(value = "/", method = RequestMethod.HEAD) @@ -201,7 +199,7 @@ public void replyToIndex(@PathVariable(value = "eventShortName", required = fals response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - var nonce = addCspHeader(response); + var nonce = addCspHeader(response, detectConfigurationLevel(eventShortName, subscriptionId), true); if (eventShortName != null && RequestUtils.isSocialMediaShareUA(userAgent) && eventRepository.existsByShortName(eventShortName)) { try (var os = response.getOutputStream(); var osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { @@ -391,7 +389,7 @@ public void getLoginPage(@RequestParam(value="failed", required = false) String try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - var nonce = addCspHeader(response); + var nonce = addCspHeader(response, false); model.addAttribute("nonce", nonce); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/login.ms"), model.asMap(), os); } @@ -434,7 +432,7 @@ public void adminHome(Model model, @Value("${alfio.version}") String version, Ht try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - var nonce = addCspHeader(response); + var nonce = addCspHeader(response, false); model.addAttribute("nonce", nonce); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/admin-index.ms"), model.asMap(), os); } @@ -455,27 +453,48 @@ private static String getNonce() { return Hex.encodeHexString(nonce); } - public String addCspHeader(HttpServletResponse response) { + public String addCspHeader(HttpServletResponse response, boolean embeddingSupported) { + return addCspHeader(response, ConfigurationLevel.system(), embeddingSupported); + } + + public String addCspHeader(HttpServletResponse response, ConfigurationLevel configurationLevel, boolean embeddingSupported) { - String nonce = getNonce(); + var nonce = getNonce(); String reportUri = ""; - var conf = configurationManager.getFor(List.of(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED, ConfigurationKeys.SECURITY_CSP_REPORT_URI), ConfigurationLevel.system()); + var conf = configurationManager.getFor(List.of(SECURITY_CSP_REPORT_ENABLED, SECURITY_CSP_REPORT_URI, EMBED_ALLOWED_ORIGINS), configurationLevel); - boolean enabledReport = conf.get(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED).getValueAsBooleanOrDefault(); + boolean enabledReport = conf.get(SECURITY_CSP_REPORT_ENABLED).getValueAsBooleanOrDefault(); if (enabledReport) { - reportUri = " report-uri " + conf.get(ConfigurationKeys.SECURITY_CSP_REPORT_URI).getValueOrDefault("/report-csp-violation"); + reportUri = " report-uri " + conf.get(SECURITY_CSP_REPORT_URI).getValueOrDefault("/report-csp-violation"); } // // https://csp.withgoogle.com/docs/strict-csp.html // with base-uri set to 'self' + var frameAncestors = "'none'"; + var allowedContainer = conf.get(EMBED_ALLOWED_ORIGINS).getValueOrNull(); + if (embeddingSupported && StringUtils.isNotBlank(allowedContainer)) { + var splitHosts = allowedContainer.split("[,\n]"); + frameAncestors = String.join(" ", splitHosts); + // IE11 + response.addHeader("X-Frame-Options", "ALLOW-FROM "+splitHosts[0]); + } else { + response.addHeader("X-Frame-Options", "DENY"); + } + response.addHeader("Content-Security-Policy", "object-src 'none'; "+ "script-src 'strict-dynamic' 'nonce-" + nonce + "' 'unsafe-inline' http: https:; " + - "base-uri 'self'; " + "base-uri 'self'; " + + "frame-ancestors " + frameAncestors + "; " + reportUri); return nonce; } + + private ConfigurationLevel detectConfigurationLevel(String eventShortName, String subscriptionId) { + return purchaseContextManager.detectConfigurationLevel(eventShortName, subscriptionId) + .orElseGet(ConfigurationLevel::system); + } } diff --git a/src/main/java/alfio/manager/PurchaseContextManager.java b/src/main/java/alfio/manager/PurchaseContextManager.java index 97fdf6b3ad..e39a4be5e8 100644 --- a/src/main/java/alfio/manager/PurchaseContextManager.java +++ b/src/main/java/alfio/manager/PurchaseContextManager.java @@ -16,10 +16,13 @@ */ package alfio.manager; +import alfio.manager.system.ConfigurationLevel; import alfio.model.PurchaseContext; import alfio.repository.EventRepository; import alfio.repository.SubscriptionRepository; import alfio.repository.TicketReservationRepository; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -28,6 +31,7 @@ @Transactional(readOnly = true) @Component +@Log4j2 public class PurchaseContextManager { private final EventRepository eventRepository; @@ -63,4 +67,23 @@ public Optional findByReservationId(String reservationId) { .map(PurchaseContext.class::cast) .or(() -> subscriptionRepository.findDescriptorByReservationId(reservationId)); } + + public Optional detectConfigurationLevel(String eventShortName, String subscriptionId) { + if (StringUtils.isAllEmpty(eventShortName, subscriptionId)) { + return Optional.empty(); + } + + try { + if (StringUtils.isNotEmpty(eventShortName)) { + return eventRepository.findOptionalEventAndOrganizationIdByShortName(eventShortName) + .map(ConfigurationLevel::event); + } + + return subscriptionRepository.findOrganizationIdForSubscription(UUID.fromString(subscriptionId)) + .map(ConfigurationLevel::organization); + } catch (Exception ex) { + log.warn("error while loading ConfigurationLevel", ex); + return Optional.empty(); + } + } } diff --git a/src/main/java/alfio/model/system/ConfigurationKeys.java b/src/main/java/alfio/model/system/ConfigurationKeys.java index 2210d37946..0f4fe5c724 100644 --- a/src/main/java/alfio/model/system/ConfigurationKeys.java +++ b/src/main/java/alfio/model/system/ConfigurationKeys.java @@ -245,6 +245,8 @@ public enum ConfigurationKeys { SECURITY_CSP_REPORT_ENABLED("Enable Content-Security-Policy reporting (default: false)", false, SettingCategory.GENERAL, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM), "false"), SECURITY_CSP_REPORT_URI("Define Content-Security-Policy reporting URI (default: /report-csp-violation)", false, SettingCategory.GENERAL, ComponentType.TEXT, false, EnumSet.of(SYSTEM)), + EMBED_ALLOWED_ORIGINS("Allowed origins for embedding the reservation process in an iFrame. Separate different origins with a newline", false, SettingCategory.GENERAL, ComponentType.TEXTAREA, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT)), + // TRANSLATION_OVERRIDE("Translation override (json)", false, SettingCategory.TRANSLATIONS, ComponentType.TEXTAREA, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT)), diff --git a/src/main/java/alfio/repository/SubscriptionRepository.java b/src/main/java/alfio/repository/SubscriptionRepository.java index 78c892af0b..2c7fce0051 100644 --- a/src/main/java/alfio/repository/SubscriptionRepository.java +++ b/src/main/java/alfio/repository/SubscriptionRepository.java @@ -156,6 +156,9 @@ int updateSubscriptionDescriptor(@Bind("title") @JSONData Map ti @Query("select * from subscription_descriptor where id = (select subscription_descriptor_fk from subscription where id = :id)") SubscriptionDescriptor findDescriptorBySubscriptionId(@Bind("id") UUID subscriptionId); + @Query("select organization_id_fk from subscription_descriptor where id = (select subscription_descriptor_fk from subscription where id = :id)") + Optional findOrganizationIdForSubscription(@Bind("id") UUID subscriptionId); + @Query("select * from subscription_descriptor_statistics where sd_organization_id_fk = :organizationId") List findAllWithStatistics(@Bind("organizationId") int organizationId);