From b17797a49bfc57e0c83e256b47c99b7bb5b4dcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Tue, 3 Mar 2026 13:19:49 +0100 Subject: [PATCH] fix(deploy): fix security --- .gitea/workflows/deploy.yaml | 1 + .gitea/workflows/pr-checks.yaml | 11 +- .../CustomQuoteRequestController.java | 75 ++++++++++- .../service/TwintPaymentService.java | 6 +- .../resources/application-local.properties | 5 +- .../src/main/resources/application.properties | 6 +- .../email/contact-request-admin.html | 121 ++++++++++++++++++ docker-compose.yml | 2 +- 8 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/resources/templates/email/contact-request-admin.html diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 591d162..ebdb49a 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -20,6 +20,7 @@ jobs: with: java-version: "21" distribution: "temurin" + cache: gradle - name: Run Tests with Gradle run: | diff --git a/.gitea/workflows/pr-checks.yaml b/.gitea/workflows/pr-checks.yaml index e341dfa..46d4971 100644 --- a/.gitea/workflows/pr-checks.yaml +++ b/.gitea/workflows/pr-checks.yaml @@ -108,7 +108,15 @@ jobs: - name: Run Gitleaks (secrets scan) shell: bash run: | - gitleaks detect --source . --no-git --redact --exit-code 1 + set +e + gitleaks detect --source . --no-git --redact --exit-code 1 \ + --report-format json --report-path /tmp/gitleaks-report.json + rc=$? + if [[ $rc -ne 0 ]]; then + echo "Gitleaks findings:" + cat /tmp/gitleaks-report.json + fi + exit $rc test-backend: runs-on: ubuntu-latest @@ -121,6 +129,7 @@ jobs: with: java-version: "21" distribution: "temurin" + cache: gradle - name: Run Tests with Gradle run: | diff --git a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java index b3918c5..b407bcb 100644 --- a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java +++ b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java @@ -1,18 +1,24 @@ package com.printcalculator.controller; +import com.printcalculator.dto.QuoteRequestDto; import com.printcalculator.entity.CustomQuoteRequest; import com.printcalculator.entity.CustomQuoteRequestAttachment; import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository; import com.printcalculator.repository.CustomQuoteRequestRepository; +import com.printcalculator.service.ClamAVService; +import com.printcalculator.service.email.EmailNotificationService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.multipart.MultipartFile; -import jakarta.validation.Valid; import java.io.IOException; import java.io.InputStream; @@ -22,8 +28,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.OffsetDateTime; +import java.time.Year; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; @@ -32,10 +41,18 @@ import java.util.regex.Pattern; @RequestMapping("/api/custom-quote-requests") public class CustomQuoteRequestController { + private static final Logger logger = LoggerFactory.getLogger(CustomQuoteRequestController.class); private final CustomQuoteRequestRepository requestRepo; private final CustomQuoteRequestAttachmentRepository attachmentRepo; - private final com.printcalculator.service.ClamAVService clamAVService; - + private final ClamAVService clamAVService; + private final EmailNotificationService emailNotificationService; + + @Value("${app.mail.contact-request.admin.enabled:true}") + private boolean contactRequestAdminMailEnabled; + + @Value("${app.mail.contact-request.admin.address:infog@3d-fab.ch}") + private String contactRequestAdminMailAddress; + // TODO: Inject Storage Service private static final Path STORAGE_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize(); private static final Pattern SAFE_EXTENSION_PATTERN = Pattern.compile("^[a-z0-9]{1,10}$"); @@ -59,17 +76,19 @@ public class CustomQuoteRequestController { public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo, CustomQuoteRequestAttachmentRepository attachmentRepo, - com.printcalculator.service.ClamAVService clamAVService) { + ClamAVService clamAVService, + EmailNotificationService emailNotificationService) { this.requestRepo = requestRepo; this.attachmentRepo = attachmentRepo; this.clamAVService = clamAVService; + this.emailNotificationService = emailNotificationService; } // 1. Create Custom Quote Request @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Transactional public ResponseEntity createCustomQuoteRequest( - @Valid @RequestPart("request") com.printcalculator.dto.QuoteRequestDto requestDto, + @Valid @RequestPart("request") QuoteRequestDto requestDto, @RequestPart(value = "files", required = false) List files ) throws IOException { if (!requestDto.isAcceptTerms() || !requestDto.isAcceptPrivacy()) { @@ -96,6 +115,7 @@ public class CustomQuoteRequestController { request = requestRepo.save(request); // 2. Handle Attachments + int attachmentsCount = 0; if (files != null && !files.isEmpty()) { if (files.size() > 15) { throw new IOException("Too many files. Max 15 allowed."); @@ -148,9 +168,12 @@ public class CustomQuoteRequestController { try (InputStream inputStream = file.getInputStream()) { Files.copy(inputStream, absolutePath, StandardCopyOption.REPLACE_EXISTING); } + attachmentsCount++; } } - + + sendAdminContactRequestNotification(request, attachmentsCount); + return ResponseEntity.ok(request); } @@ -203,4 +226,42 @@ public class CustomQuoteRequestController { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid attachment path"); } } + + private void sendAdminContactRequestNotification(CustomQuoteRequest request, int attachmentsCount) { + if (!contactRequestAdminMailEnabled) { + return; + } + if (contactRequestAdminMailAddress == null || contactRequestAdminMailAddress.isBlank()) { + logger.warn("Contact request admin notification enabled but no admin address configured."); + return; + } + + Map templateData = new HashMap<>(); + templateData.put("requestId", request.getId()); + templateData.put("createdAt", request.getCreatedAt()); + templateData.put("requestType", safeValue(request.getRequestType())); + templateData.put("customerType", safeValue(request.getCustomerType())); + templateData.put("name", safeValue(request.getName())); + templateData.put("companyName", safeValue(request.getCompanyName())); + templateData.put("contactPerson", safeValue(request.getContactPerson())); + templateData.put("email", safeValue(request.getEmail())); + templateData.put("phone", safeValue(request.getPhone())); + templateData.put("message", safeValue(request.getMessage())); + templateData.put("attachmentsCount", attachmentsCount); + templateData.put("currentYear", Year.now().getValue()); + + emailNotificationService.sendEmail( + contactRequestAdminMailAddress, + "Nuova richiesta di contatto #" + request.getId(), + "contact-request-admin", + templateData + ); + } + + private String safeValue(String value) { + if (value == null || value.isBlank()) { + return "-"; + } + return value; + } } diff --git a/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java b/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java index 539d339..6ed915b 100644 --- a/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java +++ b/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java @@ -15,13 +15,17 @@ public class TwintPaymentService { private final String twintPaymentUrl; public TwintPaymentService( - @Value("${payment.twint.url:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}") + @Value("${payment.twint.url:}") String twintPaymentUrl ) { this.twintPaymentUrl = twintPaymentUrl; } public String getTwintPaymentUrl(com.printcalculator.entity.Order order) { + if (twintPaymentUrl == null || twintPaymentUrl.isBlank()) { + throw new IllegalStateException("TWINT_PAYMENT_URL is not configured"); + } + StringBuilder urlBuilder = new StringBuilder(twintPaymentUrl); if (order != null) { diff --git a/backend/src/main/resources/application-local.properties b/backend/src/main/resources/application-local.properties index 84e4ff5..04cf953 100644 --- a/backend/src/main/resources/application-local.properties +++ b/backend/src/main/resources/application-local.properties @@ -1,7 +1,8 @@ app.mail.enabled=false app.mail.admin.enabled=false +app.mail.contact-request.admin.enabled=false # Admin back-office local test credentials -admin.password=ciaociao -admin.session.secret=local-admin-session-secret-0123456789abcdef0123456789 +admin.password=local-admin-password +admin.session.secret=local-session-secret-for-dev-only-000000000000000000000000 admin.session.ttl-minutes=480 diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index a24a15c..37dab8d 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,7 +4,7 @@ server.port=8000 # Database Configuration spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/printcalc} spring.datasource.username=${DB_USERNAME:printcalc} -spring.datasource.password=${DB_PASSWORD:printcalc_secret} +spring.datasource.password=${DB_PASSWORD:} spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.open-in-view=false @@ -26,7 +26,7 @@ clamav.port=${CLAMAV_PORT:3310} clamav.enabled=${CLAMAV_ENABLED:false} # TWINT Configuration -payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.} +payment.twint.url=${TWINT_PAYMENT_URL:} # Mail Configuration spring.mail.host=${MAIL_HOST:mail.infomaniak.com} @@ -41,6 +41,8 @@ app.mail.enabled=${APP_MAIL_ENABLED:true} app.mail.from=${APP_MAIL_FROM:${MAIL_USERNAME:noreply@printcalculator.local}} app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true} app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local} +app.mail.contact-request.admin.enabled=${APP_MAIL_CONTACT_REQUEST_ADMIN_ENABLED:true} +app.mail.contact-request.admin.address=${APP_MAIL_CONTACT_REQUEST_ADMIN_ADDRESS:infog@3d-fab.ch} app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200} # Admin back-office authentication diff --git a/backend/src/main/resources/templates/email/contact-request-admin.html b/backend/src/main/resources/templates/email/contact-request-admin.html new file mode 100644 index 0000000..4341c34 --- /dev/null +++ b/backend/src/main/resources/templates/email/contact-request-admin.html @@ -0,0 +1,121 @@ + + + + + Nuova richiesta di contatto + + + +
+

Nuova richiesta di contatto

+

E' stata ricevuta una nuova richiesta dal form contatti/su misura.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID richiesta00000000-0000-0000-0000-000000000000
Data2026-03-03T10:00:00Z
Tipo richiestaPRINT_SERVICE
Tipo clientePRIVATE
NomeMario Rossi
Azienda3D Fab SA
ContattoMario Rossi
Emailcliente@example.com
Telefono+41 00 000 00 00
MessaggioTesto richiesta cliente...
Allegati0
+ + +
+ + diff --git a/docker-compose.yml b/docker-compose.yml index 83bc72e..c144f47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: container_name: print-calculator-db environment: - POSTGRES_USER=printcalc - - POSTGRES_PASSWORD=printcalc_secret + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=printcalc ports: - "5432:5432"