fix(deploy): fix security
This commit is contained in:
@@ -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: |
|
||||||
|
|||||||
@@ -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: |
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user