fix(deploy): fix security
This commit is contained in:
@@ -20,6 +20,7 @@ jobs:
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
cache: gradle
|
||||
|
||||
- name: Run Tests with Gradle
|
||||
run: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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<CustomQuoteRequest> createCustomQuoteRequest(
|
||||
@Valid @RequestPart("request") com.printcalculator.dto.QuoteRequestDto requestDto,
|
||||
@Valid @RequestPart("request") QuoteRequestDto requestDto,
|
||||
@RequestPart(value = "files", required = false) List<MultipartFile> 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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>© <span th:text="${currentYear}">2026</span> 3D-Fab - notifica automatica.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user