fix(deploy): fix security
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / test-backend (pull_request) Successful in 45s
PR Checks / security-sast (pull_request) Failing after 28s

This commit is contained in:
2026-03-03 13:19:49 +01:00
parent a4c26ec912
commit b17797a49b
8 changed files with 213 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ jobs:
with: with:
java-version: "21" java-version: "21"
distribution: "temurin" distribution: "temurin"
cache: gradle
- name: Run Tests with Gradle - name: Run Tests with Gradle
run: | run: |

View File

@@ -108,7 +108,15 @@ jobs:
- name: Run Gitleaks (secrets scan) - name: Run Gitleaks (secrets scan)
shell: bash shell: bash
run: | 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: test-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -121,6 +129,7 @@ jobs:
with: with:
java-version: "21" java-version: "21"
distribution: "temurin" distribution: "temurin"
cache: gradle
- name: Run Tests with Gradle - name: Run Tests with Gradle
run: | run: |

View File

@@ -1,18 +1,24 @@
package com.printcalculator.controller; package com.printcalculator.controller;
import com.printcalculator.dto.QuoteRequestDto;
import com.printcalculator.entity.CustomQuoteRequest; import com.printcalculator.entity.CustomQuoteRequest;
import com.printcalculator.entity.CustomQuoteRequestAttachment; import com.printcalculator.entity.CustomQuoteRequestAttachment;
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository; import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
import com.printcalculator.repository.CustomQuoteRequestRepository; 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.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.Valid;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -22,8 +28,11 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.Year;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -32,9 +41,17 @@ import java.util.regex.Pattern;
@RequestMapping("/api/custom-quote-requests") @RequestMapping("/api/custom-quote-requests")
public class CustomQuoteRequestController { public class CustomQuoteRequestController {
private static final Logger logger = LoggerFactory.getLogger(CustomQuoteRequestController.class);
private final CustomQuoteRequestRepository requestRepo; private final CustomQuoteRequestRepository requestRepo;
private final CustomQuoteRequestAttachmentRepository attachmentRepo; 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 // TODO: Inject Storage Service
private static final Path STORAGE_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize(); private static final Path STORAGE_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize();
@@ -59,17 +76,19 @@ public class CustomQuoteRequestController {
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo, public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
CustomQuoteRequestAttachmentRepository attachmentRepo, CustomQuoteRequestAttachmentRepository attachmentRepo,
com.printcalculator.service.ClamAVService clamAVService) { ClamAVService clamAVService,
EmailNotificationService emailNotificationService) {
this.requestRepo = requestRepo; this.requestRepo = requestRepo;
this.attachmentRepo = attachmentRepo; this.attachmentRepo = attachmentRepo;
this.clamAVService = clamAVService; this.clamAVService = clamAVService;
this.emailNotificationService = emailNotificationService;
} }
// 1. Create Custom Quote Request // 1. Create Custom Quote Request
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Transactional @Transactional
public ResponseEntity<CustomQuoteRequest> createCustomQuoteRequest( public ResponseEntity<CustomQuoteRequest> createCustomQuoteRequest(
@Valid @RequestPart("request") com.printcalculator.dto.QuoteRequestDto requestDto, @Valid @RequestPart("request") QuoteRequestDto requestDto,
@RequestPart(value = "files", required = false) List<MultipartFile> files @RequestPart(value = "files", required = false) List<MultipartFile> files
) throws IOException { ) throws IOException {
if (!requestDto.isAcceptTerms() || !requestDto.isAcceptPrivacy()) { if (!requestDto.isAcceptTerms() || !requestDto.isAcceptPrivacy()) {
@@ -96,6 +115,7 @@ public class CustomQuoteRequestController {
request = requestRepo.save(request); request = requestRepo.save(request);
// 2. Handle Attachments // 2. Handle Attachments
int attachmentsCount = 0;
if (files != null && !files.isEmpty()) { if (files != null && !files.isEmpty()) {
if (files.size() > 15) { if (files.size() > 15) {
throw new IOException("Too many files. Max 15 allowed."); throw new IOException("Too many files. Max 15 allowed.");
@@ -148,9 +168,12 @@ public class CustomQuoteRequestController {
try (InputStream inputStream = file.getInputStream()) { try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, absolutePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(inputStream, absolutePath, StandardCopyOption.REPLACE_EXISTING);
} }
attachmentsCount++;
} }
} }
sendAdminContactRequestNotification(request, attachmentsCount);
return ResponseEntity.ok(request); return ResponseEntity.ok(request);
} }
@@ -203,4 +226,42 @@ public class CustomQuoteRequestController {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid attachment path"); 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<String, Object> 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;
}
} }

View File

@@ -15,13 +15,17 @@ public class TwintPaymentService {
private final String twintPaymentUrl; private final String twintPaymentUrl;
public TwintPaymentService( public TwintPaymentService(
@Value("${payment.twint.url:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}") @Value("${payment.twint.url:}")
String twintPaymentUrl String twintPaymentUrl
) { ) {
this.twintPaymentUrl = twintPaymentUrl; this.twintPaymentUrl = twintPaymentUrl;
} }
public String getTwintPaymentUrl(com.printcalculator.entity.Order order) { 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); StringBuilder urlBuilder = new StringBuilder(twintPaymentUrl);
if (order != null) { if (order != null) {

View File

@@ -1,7 +1,8 @@
app.mail.enabled=false app.mail.enabled=false
app.mail.admin.enabled=false app.mail.admin.enabled=false
app.mail.contact-request.admin.enabled=false
# Admin back-office local test credentials # Admin back-office local test credentials
admin.password=ciaociao admin.password=local-admin-password
admin.session.secret=local-admin-session-secret-0123456789abcdef0123456789 admin.session.secret=local-session-secret-for-dev-only-000000000000000000000000
admin.session.ttl-minutes=480 admin.session.ttl-minutes=480

View File

@@ -4,7 +4,7 @@ server.port=8000
# Database Configuration # Database Configuration
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/printcalc} spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/printcalc}
spring.datasource.username=${DB_USERNAME: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.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false spring.jpa.open-in-view=false
@@ -26,7 +26,7 @@ clamav.port=${CLAMAV_PORT:3310}
clamav.enabled=${CLAMAV_ENABLED:false} clamav.enabled=${CLAMAV_ENABLED:false}
# TWINT Configuration # 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 # Mail Configuration
spring.mail.host=${MAIL_HOST:mail.infomaniak.com} 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.from=${APP_MAIL_FROM:${MAIL_USERNAME:noreply@printcalculator.local}}
app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true} app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true}
app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local} 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} app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200}
# Admin back-office authentication # Admin back-office authentication

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Nuova richiesta di contatto</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 640px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
margin-top: 0;
color: #222222;
}
p {
color: #444444;
line-height: 1.5;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
th,
td {
text-align: left;
vertical-align: top;
border-bottom: 1px solid #eeeeee;
padding: 10px 6px;
color: #333333;
word-break: break-word;
}
th {
width: 35%;
color: #222222;
background: #fafafa;
}
.footer {
margin-top: 24px;
font-size: 12px;
color: #888888;
border-top: 1px solid #eeeeee;
padding-top: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>Nuova richiesta di contatto</h1>
<p>E' stata ricevuta una nuova richiesta dal form contatti/su misura.</p>
<table>
<tr>
<th>ID richiesta</th>
<td th:text="${requestId}">00000000-0000-0000-0000-000000000000</td>
</tr>
<tr>
<th>Data</th>
<td th:text="${createdAt}">2026-03-03T10:00:00Z</td>
</tr>
<tr>
<th>Tipo richiesta</th>
<td th:text="${requestType}">PRINT_SERVICE</td>
</tr>
<tr>
<th>Tipo cliente</th>
<td th:text="${customerType}">PRIVATE</td>
</tr>
<tr>
<th>Nome</th>
<td th:text="${name}">Mario Rossi</td>
</tr>
<tr>
<th>Azienda</th>
<td th:text="${companyName}">3D Fab SA</td>
</tr>
<tr>
<th>Contatto</th>
<td th:text="${contactPerson}">Mario Rossi</td>
</tr>
<tr>
<th>Email</th>
<td th:text="${email}">cliente@example.com</td>
</tr>
<tr>
<th>Telefono</th>
<td th:text="${phone}">+41 00 000 00 00</td>
</tr>
<tr>
<th>Messaggio</th>
<td th:text="${message}">Testo richiesta cliente...</td>
</tr>
<tr>
<th>Allegati</th>
<td th:text="${attachmentsCount}">0</td>
</tr>
</table>
<div class="footer">
<p>&copy; <span th:text="${currentYear}">2026</span> 3D-Fab - notifica automatica.</p>
</div>
</div>
</body>
</html>

View File

@@ -4,7 +4,7 @@ services:
container_name: print-calculator-db container_name: print-calculator-db
environment: environment:
- POSTGRES_USER=printcalc - POSTGRES_USER=printcalc
- POSTGRES_PASSWORD=printcalc_secret - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=printcalc - POSTGRES_DB=printcalc
ports: ports:
- "5432:5432" - "5432:5432"