From a7ea4d97d2f9c4a5acfd8328797a13cb30079bd6 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 30 Oct 2025 13:39:59 +0100 Subject: [PATCH] Enhanced owner validation and property checks in services: - **Backend**: - Check ownership before execution in DestinationService and PremisesService - Added Valid period check in pre flight check - Fixed allowed headers in cors config - added user groups to migration --- .../layout/config/BulkOperations.vue | 72 +++++++---- src/frontend/src/pages/Config.vue | 11 +- src/frontend/src/store/activeuser.js | 9 +- src/frontend/src/store/material.js | 1 + .../java/de/avatic/lcc/config/CorsConfig.java | 114 +++++++++--------- .../de/avatic/lcc/config/SecurityConfig.java | 8 +- .../bulk/BulkOperationController.java | 12 +- .../configuration/MaterialController.java | 4 +- .../avatic/lcc/dto/generic/LocationDTO.java | 38 ++---- .../bulk/BulkOperationRepository.java | 44 +++---- .../repositories/error/DumpRepository.java | 19 +-- .../error/SysErrorRepository.java | 4 +- .../pagination/SearchQueryResult.java | 2 +- .../premise/DestinationRepository.java | 45 +++++++ .../rates/ContainerRateRepository.java | 52 ++++---- .../service/access/DestinationService.java | 108 ++++++----------- .../lcc/service/access/PremisesService.java | 54 ++++++--- .../access/PropertyValidationService.java | 2 +- .../lcc/service/access/UserNodeService.java | 4 +- .../lcc/service/bulk/BulkImportService.java | 9 +- .../service/bulk/BulkOperationService.java | 19 ++- .../service/bulk/TemplateExportService.java | 2 +- .../steps/ContainerCalculationService.java | 34 ++---- .../PreCalculationCheckService.java | 29 ++++- .../transformer/report/ReportTransformer.java | 2 - .../service/users/AuthorizationService.java | 17 ++- .../avatic/lcc/service/users/UserService.java | 3 +- .../{V9__Material.sql => V10__Material.sql} | 0 .../resources/db/migration/V9__Groups.sql | 20 +++ src/test/resources/master_data/users.sql | 2 + 30 files changed, 425 insertions(+), 315 deletions(-) rename src/main/resources/db/migration/{V9__Material.sql => V10__Material.sql} (100%) create mode 100644 src/main/resources/db/migration/V9__Groups.sql diff --git a/src/frontend/src/components/layout/config/BulkOperations.vue b/src/frontend/src/components/layout/config/BulkOperations.vue index dc9ad2f..c8208a1 100644 --- a/src/frontend/src/components/layout/config/BulkOperations.vue +++ b/src/frontend/src/components/layout/config/BulkOperations.vue @@ -14,18 +14,25 @@
dataset
- nodes - kilometer rates + nodes - container rates + kilometer rates + + container rates + + + materials + + + packaging - materials - packaging
-
validity period
-
-
+
validity period
+
+
Import
dataset
- nodes - kilometer rates + nodes - container rates + kilometer rates + + container rates + + + materials + + + packaging - materials - packaging
file
@@ -79,8 +93,11 @@
History
-
No recent bulk operations
- +
No recent bulk + operations +
+
@@ -101,6 +118,7 @@ import {useValidityPeriodStore} from "@/store/validityPeriod.js"; import {useBulkOperationStore} from "@/store/bulkOperation.js"; import BulkOperation from "@/components/layout/bulkoperation/BulkOperation.vue"; import logger from "@/logger.js"; +import {useActiveUserStore} from "@/store/activeuser.js"; export default { name: "BulkOperations", @@ -125,13 +143,13 @@ export default { }, watch: { async isSelected(newVal) { - if(newVal === true) + if (newVal === true && this.activeUserStore.allowRates) await this.validityPeriodStore.loadPeriods(); await this.bulkOperationStore.manageStatus(); } }, computed: { - ...mapStores(useValidityPeriodStore, useBulkOperationStore), + ...mapStores(useValidityPeriodStore, useBulkOperationStore, useActiveUserStore), showValidityPeriod() { return this.exportType === "download" && (this.exportDataset === "COUNTRY_MATRIX" || this.exportDataset === "CONTAINER_RATE"); }, @@ -167,8 +185,20 @@ export default { } }, methods: { + canModify(role) { + switch (role) { + case "nodes": + return this.activeUserStore.isSuper; + case "rates": + return this.activeUserStore.isSuper || this.activeUserStore.isFreight; + case "materials": + return this.activeUserStore.isSuper || this.activeUserStore.isMaterial; + case "packaging": + return this.activeUserStore.isSuper || this.activeUserStore.isPackaging; + } + }, buildDate(date) { - if(date === null) return "not set"; + if (date === null) return "not set"; return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3]?.toString().padStart(2, '0') ?? '00'}:${date[4]?.toString().padStart(2, '0') ?? '00'}:${date[5]?.toString().padStart(2, '0') ?? '00'}` }, @@ -182,7 +212,7 @@ export default { const url = `${config.backendUrl}/bulk/templates/${this.exportDataset}/` await performDownload(url, fileName); } else { - const isCurrent = this.selectedPeriod === this.currentPeriod; + const isCurrent = this.selectedPeriod === this.currentPeriod; await this.bulkOperationStore.scheduleDownload(this.exportDataset, isCurrent ? null : this.selectedPeriod); } }, @@ -195,7 +225,7 @@ export default { this.fileBlob = await this.readFileAsBlob(file); // File-Objekt mit dem Blob erstellen, das den originalen Namen und Typ behält - this.selectedFile = new File([this.fileBlob], file.name, { type: file.type }); + this.selectedFile = new File([this.fileBlob], file.name, {type: file.type}); this.selectedFileName = file.name; logger.info(`File loaded into memory: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`); @@ -220,7 +250,7 @@ export default { reader.onload = (e) => { // ArrayBuffer in Blob konvertieren - const blob = new Blob([e.target.result], { type: file.type }); + const blob = new Blob([e.target.result], {type: file.type}); resolve(blob); }; diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue index 0062814..8989796 100644 --- a/src/frontend/src/pages/Config.vue +++ b/src/frontend/src/pages/Config.vue @@ -77,19 +77,22 @@ export default { tabsConfig() { const tabs = []; - if(this.activeUserStore.isSuper) { + if (this.activeUserStore.isSuper) { tabs.push(this.propertiesTab); tabs.push(this.systemLogTab); } - if(this.activeUserStore.isService) { + if (this.activeUserStore.isService) { tabs.push(this.appsTab); } - tabs.push(this.materialsTab); + if (this.activeUserStore.isSuper || this.activeUserStore.isMaterial) { + tabs.push(this.materialsTab); + } + tabs.push(this.nodesTab); - if(this.activeUserStore.allowRates) + if (this.activeUserStore.allowRates) tabs.push(this.ratesTab); tabs.push(this.bulkOperationsTab); diff --git a/src/frontend/src/store/activeuser.js b/src/frontend/src/store/activeuser.js index 4545498..ab74960 100644 --- a/src/frontend/src/store/activeuser.js +++ b/src/frontend/src/store/activeuser.js @@ -18,12 +18,12 @@ export const useActiveUserStore = defineStore('activeUser', { allowConfiguration(state) { if (state.user === null) return false; - return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("service"); + return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("service") || state.user.groups?.includes("material"); }, allowReporting(state) { if (state.user === null) return false; - return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("basic") || state.user.groups?.includes("calculation"); + return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("material") || state.user.groups?.includes("basic") || state.user.groups?.includes("calculation"); }, isSuper(state) { if (state.user === null) @@ -45,6 +45,11 @@ export const useActiveUserStore = defineStore('activeUser', { return false; return state.user.groups?.includes("freight"); }, + isMaterial(state) { + if (state.user === null) + return false; + return state.user.groups?.includes("material"); + }, allowRates(state) { if (state.user === null) return false; diff --git a/src/frontend/src/store/material.js b/src/frontend/src/store/material.js index 8ba9355..cbb7852 100644 --- a/src/frontend/src/store/material.js +++ b/src/frontend/src/store/material.js @@ -2,6 +2,7 @@ import {defineStore} from 'pinia' import {config} from '@/config' import {useErrorStore} from "@/store/error.js"; import performRequest from "@/backend.js"; +import logger from "@/logger.js"; export const useMaterialStore = defineStore('material', { state() { diff --git a/src/main/java/de/avatic/lcc/config/CorsConfig.java b/src/main/java/de/avatic/lcc/config/CorsConfig.java index f4d7ea7..5e828ac 100644 --- a/src/main/java/de/avatic/lcc/config/CorsConfig.java +++ b/src/main/java/de/avatic/lcc/config/CorsConfig.java @@ -14,63 +14,63 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; -@Configuration -@EnableWebMvc -@Order(Ordered.HIGHEST_PRECEDENCE) +//@Configuration +//@EnableWebMvc +//@Order(Ordered.HIGHEST_PRECEDENCE) public class CorsConfig implements WebMvcConfigurer { - private final Environment environment; - - @Value("${lcc.allowed_cors}") - private String allowedCors; - - public CorsConfig(Environment environment) { - this.environment = environment; - } - - @Override - public void addCorsMappings(@NotNull CorsRegistry registry) { - String[] activeProfiles = environment.getActiveProfiles(); - - System.out.println("Active profiles: " + Arrays.toString(activeProfiles)); - System.out.println("Allowed CORS: " + allowedCors); - - if (Arrays.asList(activeProfiles).contains("dev")) { - - System.out.println("Applying DEV CORS configuration"); - - // Development CORS configuration - registry.addMapping("/api/**") - .allowedOriginPatterns("http://localhost:*") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") - .allowCredentials(true); - - // OAuth endpoints - registry.addMapping("/oauth/**") - .allowedOriginPatterns("http://localhost:*") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } else { - - System.out.println("Applying PROD CORS configuration"); - - // Production CORS configuration - registry.addMapping("/api/**") - .allowedOrigins(allowedCors) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") - .allowCredentials(true); - - // OAuth endpoints - registry.addMapping("/oauth/**") - .allowedOriginPatterns("*") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } - } +// private final Environment environment; +// +// @Value("${lcc.allowed_cors}") +// private String allowedCors; +// +// public CorsConfig(Environment environment) { +// this.environment = environment; +// } +// +// @Override +// public void addCorsMappings(@NotNull CorsRegistry registry) { +// String[] activeProfiles = environment.getActiveProfiles(); +// +// System.out.println("Active profiles: " + Arrays.toString(activeProfiles)); +// System.out.println("Allowed CORS: " + allowedCors); +// +// if (Arrays.asList(activeProfiles).contains("dev")) { +// +// System.out.println("Applying DEV CORS configuration"); +// +// // Development CORS configuration +// registry.addMapping("/api/**") +// .allowedOriginPatterns("http://localhost:*") +// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") +// .allowCredentials(true); +// +// // OAuth endpoints +// registry.addMapping("/oauth/**") +// .allowedOriginPatterns("http://localhost:*") +// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .allowCredentials(true); +// } else { +// +// System.out.println("Applying PROD CORS configuration"); +// +// // Production CORS configuration +// registry.addMapping("/api/**") +// .allowedOrigins(allowedCors) +// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") +// .allowCredentials(true); +// +// // OAuth endpoints +// registry.addMapping("/oauth/**") +// .allowedOriginPatterns("*") +// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .allowCredentials(true); +// } +// } } \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index 4667294..7a65bc0 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -104,10 +104,13 @@ public class SecurityConfig { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); + configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; @@ -131,6 +134,9 @@ public class SecurityConfig { configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); + configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; diff --git a/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java b/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java index 2a6d774..4de5e26 100644 --- a/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java +++ b/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java @@ -37,20 +37,20 @@ public class BulkOperationController { } @GetMapping({"/status/", "/status"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity> getBulkStatus() { return ResponseEntity.ok(bulkOperationService.getStatus()); } @PostMapping({"/upload/{type}", "/upload/{type}/"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity uploadFile(@PathVariable BulkFileType type, @BodyParam("file") MultipartFile file) { bulkOperationService.processFileImport(type, file); return ResponseEntity.ok().build(); } @GetMapping({"/templates/{type}", "/templates/{type}/"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity generateTemplate(@PathVariable BulkFileType type) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Disposition", "attachment; filename=lcc_template_" + type.name().toLowerCase() + ".xlsx"); @@ -64,7 +64,7 @@ public class BulkOperationController { @GetMapping({"/download/{type}", "/download/{type}/"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity scheduleDownload(@PathVariable BulkFileType type) { bulkOperationService.processFileExport(type); return ResponseEntity.ok().build(); @@ -72,7 +72,7 @@ public class BulkOperationController { @GetMapping({"/download/{type}/{validity_period_id}", "/download/{type}/{validity_period_id}/"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity scheduleDownload(@PathVariable BulkFileType type, @PathVariable("validity_period_id") Integer validityPeriodId) { bulkOperationService.processFileExport(type, validityPeriodId); return ResponseEntity.ok().build(); @@ -80,7 +80,7 @@ public class BulkOperationController { } @GetMapping({"/file/{processId}", "/file/{processId}/"}) - @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity download(@PathVariable("processId") Integer id) { var op = bulkOperationService.getBulkOperation(id); diff --git a/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java b/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java index 542f131..c5e3b98 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java @@ -37,7 +37,7 @@ public class MaterialController { * X-Total-Count (total elements), X-Page-Count (total pages), and X-Current-Page (current page). */ @GetMapping("/") - @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity> listMaterials( @RequestParam(defaultValue = "true") String excludeDeprecated, @RequestParam(defaultValue = "20") @Min(1) int limit, @@ -61,7 +61,7 @@ public class MaterialController { * @throws RuntimeException if the material with the given ID is not found. */ @GetMapping("/{id}") - @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC', 'FREIGHT', 'PACKAGING')") + @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC', 'FREIGHT', 'PACKAGING', 'MATERIAL')") public ResponseEntity getMaterialDetails(@PathVariable Integer id) { return ResponseEntity.ok(materialService.getMaterial(id)); } diff --git a/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java b/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java index 3ecc6ab..ae02f94 100644 --- a/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java +++ b/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java @@ -1,26 +1,15 @@ package de.avatic.lcc.dto.generic; -import com.fasterxml.jackson.annotation.JsonFormat; - -import java.util.Objects; - /** * Represents a geographical location with latitude and longitude. * This immutable DTO (Data Transfer Object) is used to transfer location data across system layers. + * + * @param latitude The latitude of the location. + * Positive values indicate north and negative values indicate south. + * @param longitude The longitude of the location. + * Positive values indicate east and negative values indicate west. */ -public class LocationDTO { - - /** - * The latitude of the location. - * Positive values indicate north and negative values indicate south. - */ - private final double latitude; - - /** - * The longitude of the location. - * Positive values indicate east and negative values indicate west. - */ - private final double longitude; +public record LocationDTO(double latitude, double longitude) { /** * Constructs a new {@code LocationDTO} with the specified latitude and longitude. @@ -28,9 +17,7 @@ public class LocationDTO { * @param latitude the latitude of the location, where north is positive and south is negative * @param longitude the longitude of the location, where east is positive and west is negative */ - public LocationDTO(double latitude, double longitude) { - this.latitude = latitude; - this.longitude = longitude; + public LocationDTO { } /** @@ -46,7 +33,8 @@ public class LocationDTO { * * @return the latitude, where positive values indicate north and negative values indicate south */ - public double getLatitude() { + @Override + public double latitude() { return latitude; } @@ -55,7 +43,8 @@ public class LocationDTO { * * @return the longitude, where positive values indicate east and negative values indicate west */ - public double getLongitude() { + @Override + public double longitude() { return longitude; } @@ -68,11 +57,6 @@ public class LocationDTO { Double.compare(that.longitude, longitude) == 0; } - @Override - public int hashCode() { - return Objects.hash(latitude, longitude); - } - @Override public String toString() { return "LocationDTO{" + diff --git a/src/main/java/de/avatic/lcc/repositories/bulk/BulkOperationRepository.java b/src/main/java/de/avatic/lcc/repositories/bulk/BulkOperationRepository.java index 1fc0201..bd91cb5 100644 --- a/src/main/java/de/avatic/lcc/repositories/bulk/BulkOperationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/bulk/BulkOperationRepository.java @@ -160,38 +160,32 @@ public class BulkOperationRepository { ); } - private static class BulkOperationRowMapper implements RowMapper { + private record BulkOperationRowMapper(boolean skipFile) implements RowMapper { - private final boolean skipFile; - - BulkOperationRowMapper() { - this(false); - } - - BulkOperationRowMapper(boolean skipFile) { - this.skipFile = skipFile; - } + BulkOperationRowMapper() { + this(false); + } @Override - public BulkOperation mapRow(ResultSet rs, int rowNum) throws SQLException { - BulkOperation operation = new BulkOperation(); - operation.setId(rs.getInt("id")); - operation.setUserId(rs.getInt("user_id")); - operation.setProcessingType(BulkProcessingType.valueOf(rs.getString("bulk_processing_type"))); - operation.setFileType(BulkFileType.valueOf(rs.getString("bulk_file_type"))); - operation.setProcessState(BulkOperationState.valueOf(rs.getString("state"))); + public BulkOperation mapRow(ResultSet rs, int rowNum) throws SQLException { + BulkOperation operation = new BulkOperation(); + operation.setId(rs.getInt("id")); + operation.setUserId(rs.getInt("user_id")); + operation.setProcessingType(BulkProcessingType.valueOf(rs.getString("bulk_processing_type"))); + operation.setFileType(BulkFileType.valueOf(rs.getString("bulk_file_type"))); + operation.setProcessState(BulkOperationState.valueOf(rs.getString("state"))); - operation.setValidityPeriodId(rs.getInt("validity_period_id")); - if (rs.wasNull()) - operation.setValidityPeriodId(null); + operation.setValidityPeriodId(rs.getInt("validity_period_id")); + if (rs.wasNull()) + operation.setValidityPeriodId(null); - if (!skipFile) - operation.setFile(rs.getBytes("file")); - operation.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); - return operation; + if (!skipFile) + operation.setFile(rs.getBytes("file")); + operation.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); + return operation; + } } - } } diff --git a/src/main/java/de/avatic/lcc/repositories/error/DumpRepository.java b/src/main/java/de/avatic/lcc/repositories/error/DumpRepository.java index 9082fb5..a0d7eed 100644 --- a/src/main/java/de/avatic/lcc/repositories/error/DumpRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/error/DumpRepository.java @@ -30,15 +30,18 @@ import java.util.Map; @Repository public class DumpRepository { - @Autowired - private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired - private PremiseRepository premiseRepository; - @Autowired - private PremiseTransformer premiseTransformer; + private final JdbcTemplate jdbcTemplate; + private final PremiseRepository premiseRepository; + private final PremiseTransformer premiseTransformer; + + public DumpRepository(NamedParameterJdbcTemplate namedParameterJdbcTemplate, JdbcTemplate jdbcTemplate, PremiseRepository premiseRepository, PremiseTransformer premiseTransformer) { + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + this.jdbcTemplate = jdbcTemplate; + this.premiseRepository = premiseRepository; + this.premiseTransformer = premiseTransformer; + } @Transactional(readOnly = true) public CalculationJobDumpDTO getDump(Integer id) { diff --git a/src/main/java/de/avatic/lcc/repositories/error/SysErrorRepository.java b/src/main/java/de/avatic/lcc/repositories/error/SysErrorRepository.java index 94d2726..0f3ae12 100644 --- a/src/main/java/de/avatic/lcc/repositories/error/SysErrorRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/error/SysErrorRepository.java @@ -122,7 +122,7 @@ public class SysErrorRepository { } // Count total elements - String countSql = "SELECT COUNT(*) FROM sys_error e" + whereClause.toString(); + String countSql = "SELECT COUNT(*) FROM sys_error e" + whereClause; Integer totalElements = namedParameterJdbcTemplate.queryForObject(countSql, parameters, Integer.class); // Build main query with pagination @@ -130,7 +130,7 @@ public class SysErrorRepository { SELECT e.id, e.user_id, e.title, e.code, e.message, e.pinia, e.calculation_job_id, e.bulk_operation_id, e.type, e.created_at FROM sys_error e - """ + whereClause.toString() + """ + """ + whereClause + """ ORDER BY e.created_at DESC LIMIT :limit OFFSET :offset """; diff --git a/src/main/java/de/avatic/lcc/repositories/pagination/SearchQueryResult.java b/src/main/java/de/avatic/lcc/repositories/pagination/SearchQueryResult.java index 1070b6e..ad98b49 100644 --- a/src/main/java/de/avatic/lcc/repositories/pagination/SearchQueryResult.java +++ b/src/main/java/de/avatic/lcc/repositories/pagination/SearchQueryResult.java @@ -14,7 +14,7 @@ import java.util.function.Function; */ public class SearchQueryResult { - private List result; + private final List result; private final Integer page, totalPages, totalElements, elementsPerPage; diff --git a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java index 3ac7311..7b0c996 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java @@ -135,6 +135,28 @@ public class DestinationRepository { return Optional.of(userId.getFirst()); } + @Transactional + public Map getOwnerIdsByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyMap(); + } + + String placeholders = String.join(",", Collections.nCopies(ids.size(), "?")); + String query = String.format(""" + SELECT pd.id AS pd_id, p.user_id AS user_id + FROM premise_destination pd + JOIN premise p ON pd.premise_id = p.id + WHERE pd.id IN (%s)""", placeholders); + + return jdbcTemplate.query(query, rs -> { + Map result = new HashMap<>(); + while (rs.next()) { + result.put(rs.getInt("pd_id"), rs.getInt("user_id")); + } + return result; + }, ids.toArray()); + } + @Transactional public List getByPremiseIdsAndNodeId(List premiseId, Integer nodeId, Integer userId) { String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?")); @@ -226,6 +248,29 @@ public class DestinationRepository { } } + @Transactional + public void checkOwner(List ids, Integer userId) { + if (ids == null || ids.isEmpty()) { + return; + } + + Map ownerMap = getOwnerIdsByIds(ids); + List violations = new ArrayList<>(); + + for (Integer id : ids) { + Integer ownerId = ownerMap.get(id); + + if (ownerId == null || !ownerId.equals(userId)) { + violations.add("id " + id + " (owner: " + ownerId + " user: " + userId + ")"); + } + } + + if (!violations.isEmpty()) { + throw new ForbiddenException("Access violation. Accessing destinations: " + + String.join(", ", violations)); + } + } + private static class DestinationMapper implements RowMapper { @Override diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java index 55f1fb9..257e1e3 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -288,38 +288,32 @@ public class ContainerRateRepository { } - private static class ContainerRateMapper implements RowMapper { - - private final boolean fetchCountryIds; - - public ContainerRateMapper(boolean fetchCountryIds) { - this.fetchCountryIds = fetchCountryIds; - } + private record ContainerRateMapper(boolean fetchCountryIds) implements RowMapper { public ContainerRateMapper() { - this(false); - } - - @Override - public ContainerRate mapRow(ResultSet rs, int rowNum) throws SQLException { - var entity = new ContainerRate(); - - entity.setId(rs.getInt("id")); - entity.setValidityPeriodId(rs.getInt("validity_period_id")); - entity.setFromNodeId(rs.getInt("from_node_id")); - entity.setToNodeId(rs.getInt("to_node_id")); - entity.setType(TransportType.valueOf(rs.getString("container_rate_type"))); - entity.setLeadTime(rs.getInt("lead_time")); - entity.setRateFeu(rs.getBigDecimal("rate_feu")); - entity.setRateTeu(rs.getBigDecimal("rate_teu")); - entity.setRateHc(rs.getBigDecimal("rate_hc")); - - if (fetchCountryIds) { - entity.setToCountryId(rs.getInt("to_country_id")); - entity.setFromCountryId(rs.getInt("from_country_id")); + this(false); } - return entity; + @Override + public ContainerRate mapRow(ResultSet rs, int rowNum) throws SQLException { + var entity = new ContainerRate(); + + entity.setId(rs.getInt("id")); + entity.setValidityPeriodId(rs.getInt("validity_period_id")); + entity.setFromNodeId(rs.getInt("from_node_id")); + entity.setToNodeId(rs.getInt("to_node_id")); + entity.setType(TransportType.valueOf(rs.getString("container_rate_type"))); + entity.setLeadTime(rs.getInt("lead_time")); + entity.setRateFeu(rs.getBigDecimal("rate_feu")); + entity.setRateTeu(rs.getBigDecimal("rate_teu")); + entity.setRateHc(rs.getBigDecimal("rate_hc")); + + if (fetchCountryIds) { + entity.setToCountryId(rs.getInt("to_country_id")); + entity.setFromCountryId(rs.getInt("from_country_id")); + } + + return entity; + } } - } } diff --git a/src/main/java/de/avatic/lcc/service/access/DestinationService.java b/src/main/java/de/avatic/lcc/service/access/DestinationService.java index 60f10f3..384978f 100644 --- a/src/main/java/de/avatic/lcc/service/access/DestinationService.java +++ b/src/main/java/de/avatic/lcc/service/access/DestinationService.java @@ -15,7 +15,6 @@ import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.service.calculation.RoutingService; import de.avatic.lcc.service.transformer.premise.DestinationTransformer; import de.avatic.lcc.service.users.AuthorizationService; -import de.avatic.lcc.util.exception.base.ForbiddenException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,15 +33,12 @@ public class DestinationService { private final RouteSectionRepository routeSectionRepository; private final RouteNodeRepository routeNodeRepository; private final RoutingService routingService; - ; private final NodeRepository nodeRepository; private final PremiseRepository premiseRepository; private final UserNodeRepository userNodeRepository; - private final PropertyRepository propertyRepository; - private final PropertyService propertyService; private final AuthorizationService authorizationService; - public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, PropertyRepository propertyRepository, PropertyService propertyService, AuthorizationService authorizationService) { + public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService) { this.destinationRepository = destinationRepository; this.destinationTransformer = destinationTransformer; this.routeRepository = routeRepository; @@ -52,16 +48,16 @@ public class DestinationService { this.nodeRepository = nodeRepository; this.premiseRepository = premiseRepository; this.userNodeRepository = userNodeRepository; - this.propertyRepository = propertyRepository; - this.propertyService = propertyService; this.authorizationService = authorizationService; } @Transactional public Map> setDestination(DestinationSetDTO dto) { - //TODO fix user authorization - var userId = 1; - //dto.getPremiseId().forEach(id -> destinationRepository.checkOwner(id, userId)); + var admin = authorizationService.isSuper(); + Integer userId = authorizationService.getUserId(); + + if (!admin) + destinationRepository.checkOwner(dto.getPremiseId(), userId); deleteAllDestinationsByPremiseId(dto.getPremiseId(), false); @@ -80,7 +76,6 @@ public class DestinationService { } - private List createDestination(List premisesToProcess, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map> routes) { Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow(); @@ -141,17 +136,22 @@ public class DestinationService { } public DestinationDTO getDestination(Integer id) { - //todo check authorization + var admin = authorizationService.isSuper(); + var userId = authorizationService.getUserId(); + + if (!admin) + destinationRepository.checkOwner(id, userId); return destinationTransformer.toDestinationDTO(destinationRepository.getById(id).orElseThrow()); } @Transactional public void updateDestination(Integer id, DestinationUpdateDTO destinationUpdateDTO) { - //todo check authorization + var admin = authorizationService.isSuper(); Integer userId = authorizationService.getUserId(); - destinationRepository.checkOwner(id, userId); + if (!admin) + destinationRepository.checkOwner(id, userId); var selectedRouteId = destinationUpdateDTO.getRouteSelectedId(); @@ -159,7 +159,6 @@ public class DestinationService { routeRepository.updateSelectedByDestinationId(id, selectedRouteId); } - destinationRepository.update(id, destinationUpdateDTO.getAnnualAmount(), destinationUpdateDTO.getRepackingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRepackingCost().doubleValue()), @@ -177,21 +176,21 @@ public class DestinationService { Map nodes = new HashMap<>(); Map userNodes = new HashMap<>(); - for(var premise : premisses) { - for(var destinationId : destinationIds) { + for (var premise : premisses) { + for (var destinationId : destinationIds) { boolean isUserSupplierNode = (premise.getSupplierNodeId() == null); var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode); - if(routes.containsKey(ids)) continue; + if (routes.containsKey(ids)) continue; - if(!nodes.containsKey(destinationId)) { + if (!nodes.containsKey(destinationId)) { nodes.put(destinationId, nodeRepository.getById(destinationId).orElseThrow()); } - if(!isUserSupplierNode && !nodes.containsKey(premise.getSupplierNodeId())) { + if (!isUserSupplierNode && !nodes.containsKey(premise.getSupplierNodeId())) { nodes.put(premise.getSupplierNodeId(), nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow()); } - if(isUserSupplierNode && !userNodes.containsKey(premise.getUserSupplierNodeId())) { + if (isUserSupplierNode && !userNodes.containsKey(premise.getUserSupplierNodeId())) { userNodes.put(premise.getUserSupplierNodeId(), userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow()); } @@ -205,7 +204,6 @@ public class DestinationService { @Transactional public void saveRoute(List routeObjs, Integer destinationId) { - for (var routeObj : routeObjs) { boolean first = true; Integer fromNodeId = null; @@ -238,44 +236,12 @@ public class DestinationService { } } - private record RouteIds(Integer fromNodeId, Integer toNodeId, boolean isUserSupplierNode) {} - - @Transactional public void findRouteAndSave(Integer destinationId, Node destination, Node supplier, boolean isUserSupplierNode) { var routeObjs = routingService.findRoutes(destination, supplier, isUserSupplierNode); - for (var routeObj : routeObjs) { - boolean first = true; - Integer fromNodeId = null; - - var premiseRoute = routeObj.getRoute(); - premiseRoute.setDestinationId(destinationId); - int routeId = routeRepository.insert(premiseRoute); - - - for (RouteSectionInformation section : routeObj.getSections()) { - - if (first) { - fromNodeId = routeNodeRepository.insert(section.getFromNode()); - first = false; - } - - var toNode = section.getToNode(); - Integer toNodeId = routeNodeRepository.insert(toNode); - - var premiseRouteSection = section.getSection(); - - premiseRouteSection.setRouteId(routeId); - premiseRouteSection.setFromRouteNodeId(fromNodeId); - premiseRouteSection.setToRouteNodeId(toNodeId); - - routeSectionRepository.insert(premiseRouteSection); - - fromNodeId = toNodeId; - } - } + saveRoute(routeObjs, destinationId); } @Transactional @@ -289,31 +255,26 @@ public class DestinationService { destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly)); } - @Transactional public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) { - //todo check authorization + var admin = authorizationService.isSuper(); Integer userId = authorizationService.getUserId(); - Optional ownerId = destinationRepository.getOwnerIdById(id); - if (ownerId.isPresent() && ownerId.get().equals(userId)) { - List routes = routeRepository.getByDestinationId(id); + if (!admin) + destinationRepository.checkOwner(id, userId); - for (var route : routes) { - List sections = routeSectionRepository.getByRouteId(route.getId()); - routeSectionRepository.deleteAllById(sections.stream().map(RouteSection::getId).toList()); - routeNodeRepository.deleteAllById(sections.stream().flatMap(section -> Stream.of(section.getFromRouteNodeId(), section.getToRouteNodeId())).toList()); - } + List routes = routeRepository.getByDestinationId(id); - routeRepository.deleteAllById(routes.stream().map(Route::getId).toList()); - - if (!deleteRoutesOnly) - destinationRepository.deleteById(id); - - return; + for (var route : routes) { + List sections = routeSectionRepository.getByRouteId(route.getId()); + routeSectionRepository.deleteAllById(sections.stream().map(RouteSection::getId).toList()); + routeNodeRepository.deleteAllById(sections.stream().flatMap(section -> Stream.of(section.getFromRouteNodeId(), section.getToRouteNodeId())).toList()); } - throw new ForbiddenException("Not authorized to delete destination with id " + id); + routeRepository.deleteAllById(routes.stream().map(Route::getId).toList()); + + if (!deleteRoutesOnly) + destinationRepository.deleteById(id); } @@ -349,5 +310,8 @@ public class DestinationService { } } + private record RouteIds(Integer fromNodeId, Integer toNodeId, boolean isUserSupplierNode) { + } + } diff --git a/src/main/java/de/avatic/lcc/service/access/PremisesService.java b/src/main/java/de/avatic/lcc/service/access/PremisesService.java index 3b6b037..ca9f5a0 100644 --- a/src/main/java/de/avatic/lcc/service/access/PremisesService.java +++ b/src/main/java/de/avatic/lcc/service/access/PremisesService.java @@ -90,20 +90,25 @@ public class PremisesService { @Transactional(readOnly = true) public List getPremises(List premiseIds) { + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(premiseIds, userId); + + if (!admin) + premiseRepository.checkOwner(premiseIds, userId); return premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(premiseTransformer::toPremiseDetailDTO).toList(); } public void startCalculation(List premises) { - + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(premises, userId); + + if (!admin) + premiseRepository.checkOwner(premises, userId); var validSetId = propertySetRepository.getValidSetId(); - var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no valid period found that is VALID")); + var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no set of transport rates found that is VALID")); premises.forEach(premiseId -> preCalculationCheckService.doPrecheck(premiseId, validSetId, validPeriodId)); @@ -183,12 +188,13 @@ public class PremisesService { return calculationStatusService.getCalculationStatus(processId); } - @Transactional - public void updatePackaging(PackagingUpdateDTO packagingDTO) { - //TODO check values. and return errors if needed + public void updatePackaging(PackagingUpdateDTO packagingDTO) { + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(packagingDTO.getPremiseIds(), userId); + + if (!admin) + premiseRepository.checkOwner(packagingDTO.getPremiseIds(), userId); var dimensions = packagingDTO.getDimensions() == null ? null : dimensionTransformer.toDimensionEntity(packagingDTO.getDimensions()); @@ -197,9 +203,11 @@ public class PremisesService { } public void updateMaterial(MaterialUpdateDTO materialUpdateDTO) { - //TODO check values. and return errors if needed + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId); + + if (!admin) + premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId); var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue()); premiseRepository.updateMaterial(materialUpdateDTO.getPremiseIds(), materialUpdateDTO.getHsCode(), tariffRate); @@ -207,9 +215,11 @@ public class PremisesService { } public void updatePrice(PriceUpdateDTO priceUpdateDTO) { - //TODO check values. and return errors if needed + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(priceUpdateDTO.getPremiseIds(), userId); + + if (!admin) + premiseRepository.checkOwner(priceUpdateDTO.getPremiseIds(), userId); var price = priceUpdateDTO.getPrice() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getPrice().doubleValue()); var overseaShare = priceUpdateDTO.getOverseaShare() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getOverseaShare().doubleValue()); @@ -219,8 +229,12 @@ public class PremisesService { @Transactional public void delete(List premiseIds) { + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(premiseIds, userId); + + if (!admin) + premiseRepository.checkOwner(premiseIds, userId); + // only delete drafts. var toBeDeleted = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(Premise::getId).toList(); @@ -232,9 +246,14 @@ public class PremisesService { } + @Transactional public void archive(List premiseIds) { + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(premiseIds, userId); + + if (!admin) + premiseRepository.checkOwner(premiseIds, userId); + // only archive completed. var premisses = premiseRepository.getPremisesById(premiseIds); @@ -262,9 +281,14 @@ public class PremisesService { return resolveDTO; } + @Transactional public List duplicate(List premissIds) { + var admin = authorizationService.isSuper(); var userId = authorizationService.getUserId(); - premiseRepository.checkOwner(premissIds, userId); + + if (!admin) + premiseRepository.checkOwner(premissIds, userId); + var newIds = new ArrayList(); premissIds.forEach(id -> { diff --git a/src/main/java/de/avatic/lcc/service/access/PropertyValidationService.java b/src/main/java/de/avatic/lcc/service/access/PropertyValidationService.java index 5273a60..49f626f 100644 --- a/src/main/java/de/avatic/lcc/service/access/PropertyValidationService.java +++ b/src/main/java/de/avatic/lcc/service/access/PropertyValidationService.java @@ -172,7 +172,7 @@ public class PropertyValidationService { public void evaluate(String value) { if (operator == CompareOperator.ENUM) { if (!((List) this.value).contains(value)) { - throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((List) this.value).toString(), value); + throw new PropertyValidationException(propertyId, operator.getIdentifier(), this.value.toString(), value); } } else { try { diff --git a/src/main/java/de/avatic/lcc/service/access/UserNodeService.java b/src/main/java/de/avatic/lcc/service/access/UserNodeService.java index 85b7596..7ef8b3a 100644 --- a/src/main/java/de/avatic/lcc/service/access/UserNodeService.java +++ b/src/main/java/de/avatic/lcc/service/access/UserNodeService.java @@ -53,8 +53,8 @@ public class UserNodeService { node.setName(dto.getName()); node.setAddress(dto.getAddress()); - node.setGeoLng(BigDecimal.valueOf(dto.getLocation().getLongitude())); - node.setGeoLat(BigDecimal.valueOf(dto.getLocation().getLatitude())); + node.setGeoLng(BigDecimal.valueOf(dto.getLocation().longitude())); + node.setGeoLat(BigDecimal.valueOf(dto.getLocation().latitude())); node.setDestination(false); node.setSource(true); node.setIntermediate(false); diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java index acecd03..9acb10b 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java @@ -2,11 +2,9 @@ package de.avatic.lcc.service.bulk; import de.avatic.lcc.model.bulk.BulkFileTypes; import de.avatic.lcc.model.bulk.BulkOperation; -import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.service.api.BatchGeoApiService; import de.avatic.lcc.service.bulk.bulkImport.*; import de.avatic.lcc.service.excelMapper.*; -import de.avatic.lcc.service.transformer.generic.NodeTransformer; import de.avatic.lcc.util.exception.internalerror.ExcelValidationError; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -26,8 +24,7 @@ public class BulkImportService { private final MaterialExcelMapper materialExcelMapper; private final PackagingExcelMapper packagingExcelMapper; private final NodeExcelMapper nodeExcelMapper; - private final NodeRepository nodeRepository; - private final NodeTransformer nodeTransformer; + private final NodeBulkImportService nodeBulkImportService; private final PackagingBulkImportService packagingBulkImportService; private final MaterialBulkImportService materialBulkImportService; @@ -35,14 +32,12 @@ public class BulkImportService { private final ContainerRateImportService containerRateImportService; private final BatchGeoApiService batchGeoApiService; - public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService) { + public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService) { this.matrixRateExcelMapper = matrixRateExcelMapper; this.containerRateExcelMapper = containerRateExcelMapper; this.materialExcelMapper = materialExcelMapper; this.packagingExcelMapper = packagingExcelMapper; this.nodeExcelMapper = nodeExcelMapper; - this.nodeRepository = nodeRepository; - this.nodeTransformer = nodeTransformer; this.nodeBulkImportService = nodeBulkImportService; this.packagingBulkImportService = packagingBulkImportService; this.materialBulkImportService = materialBulkImportService; diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkOperationService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkOperationService.java index 756e4b8..7aaff8a 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkOperationService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkOperationService.java @@ -10,7 +10,9 @@ import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.service.transformer.bulk.BulkOperationTransformer; import de.avatic.lcc.service.users.AuthorizationService; import de.avatic.lcc.util.exception.badrequest.FileFormatNotSupportedException; +import de.avatic.lcc.util.exception.base.ForbiddenException; import de.avatic.lcc.util.exception.base.InternalErrorException; +import de.avatic.lcc.util.exception.internalerror.ExcelValidationError; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -39,6 +41,8 @@ public class BulkOperationService { public void processFileImport(BulkFileType fileType, MultipartFile file) { var userId = authorizationService.getUserId(); + checkAuthorized(fileType); + String contentType = file.getContentType(); if (!"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".equals(contentType) && @@ -62,10 +66,23 @@ public class BulkOperationService { } catch (IOException e) { - throw new RuntimeException(e); //TODO throw a nice exception + throw new ExcelValidationError("Unable to read uploaded file."); } } + private void checkAuthorized(BulkFileType fileType) { + + if( switch (fileType) { + case CONTAINER_RATE, COUNTRY_MATRIX -> authorizationService.hasAnyRole("SUPER", "FREIGHT"); + case MATERIAL -> authorizationService.hasAnyRole("SUPER", "MATERIAL"); + case PACKAGING -> authorizationService.hasAnyRole("SUPER", "PACKAGING"); + case NODE -> authorizationService.hasAnyRole("SUPER"); + }) { + throw new ForbiddenException("You are not authorized to perform this operation"); + } + + } + public void processFileExport(BulkFileType type, Integer validityPeriodId) { var userId = authorizationService.getUserId(); diff --git a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java index 89883ed..e5159f4 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java @@ -49,7 +49,7 @@ public class TemplateExportService { public InputStreamSource generateTemplate(BulkFileType bulkFileType) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Workbook workbook = new XSSFWorkbook();) { + Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName()); diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java index b3ce8fa..3ef74ca 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/ContainerCalculationService.java @@ -229,7 +229,7 @@ public class ContainerCalculationService { } - private static enum SolutionType { + private enum SolutionType { HORIZONTAL, VERTICAL } @@ -289,30 +289,22 @@ public class ContainerCalculationService { } @Renderer(text = "getFullDebugInfo()") - private static class Block { - - private final PackagingDimension dimension; - private final BlockType type; - - public Block(PackagingDimension dimension, BlockType type) { - this.dimension = dimension; - this.type = type; - } + private record Block(PackagingDimension dimension, BlockType type) { public int calculateBlockCount(int containerColumnLength) { - return containerColumnLength / type.getBlockColumnLength(dimension); - } + return containerColumnLength / type.getBlockColumnLength(dimension); + } - public int getResultValue() { - return type.getResultValue(); - } + public int getResultValue() { + return type.getResultValue(); + } - public int getBlockWidth() { - return type.getColumnWidth(dimension); - } + public int getBlockWidth() { + return type.getColumnWidth(dimension); + } - public String getFullDebugInfo() { - return "BLOCK %s W%s L%s H%s".formatted(type.name(), type.getColumnWidth(dimension), type.getBlockColumnLength(dimension), dimension.getHeight()); + public String getFullDebugInfo() { + return "BLOCK %s W%s L%s H%s".formatted(type.name(), type.getColumnWidth(dimension), type.getBlockColumnLength(dimension), dimension.getHeight()); + } } - } } diff --git a/src/main/java/de/avatic/lcc/service/precalculation/PreCalculationCheckService.java b/src/main/java/de/avatic/lcc/service/precalculation/PreCalculationCheckService.java index 72b67aa..21fe638 100644 --- a/src/main/java/de/avatic/lcc/service/precalculation/PreCalculationCheckService.java +++ b/src/main/java/de/avatic/lcc/service/precalculation/PreCalculationCheckService.java @@ -14,6 +14,7 @@ import de.avatic.lcc.model.db.rates.ValidityPeriodState; import de.avatic.lcc.model.db.utils.WeightUnit; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.premise.*; +import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.properties.PropertySetRepository; import de.avatic.lcc.repositories.rates.ContainerRateRepository; import de.avatic.lcc.repositories.rates.MatrixRateRepository; @@ -25,9 +26,12 @@ import de.avatic.lcc.util.exception.internalerror.PremiseValidationError; import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalUnit; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; @Service public class PreCalculationCheckService { @@ -48,8 +52,9 @@ public class PreCalculationCheckService { private final ContainerRateRepository containerRateRepository; private final ValidityPeriodRepository validityPeriodRepository; private final PropertySetRepository propertySetRepository; + private final PropertyRepository propertyRepository; - public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, ValidityPeriodRepository validityPeriodRepository, PropertySetRepository propertySetRepository) { + public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, ValidityPeriodRepository validityPeriodRepository, PropertySetRepository propertySetRepository, PropertyRepository propertyRepository) { this.premiseRepository = premiseRepository; this.customApiService = customApiService; this.destinationRepository = destinationRepository; @@ -64,6 +69,7 @@ public class PreCalculationCheckService { this.containerRateRepository = containerRateRepository; this.validityPeriodRepository = validityPeriodRepository; this.propertySetRepository = propertySetRepository; + this.propertyRepository = propertyRepository; } public void doPrecheck(Integer premiseId, Integer setId, Integer periodId) { @@ -125,17 +131,30 @@ public class PreCalculationCheckService { private void periodCheck(ValidityPeriod period, PropertySet set) { + if(set == null) - throw new PremiseValidationError("There are no system properties for the given date."); + throw new PremiseValidationError("There are no system properties for the given date. Please contact your administrator."); if(period == null) - throw new PremiseValidationError("There are no rates for the given date."); + throw new PremiseValidationError("There are no rates for the given date. Please contact your administrator."); if(ValidityPeriodState.VALID != period.getState() && ValidityPeriodState.EXPIRED != period.getState()) - throw new PremiseValidationError("There are no valid rates for the given date."); + throw new PremiseValidationError("There are no valid rates for the given date. Please contact your administrator."); if(ValidityPeriodState.VALID != set.getState() && ValidityPeriodState.EXPIRED != period.getState()) - throw new PremiseValidationError("There are no valid system properties for the given date."); + throw new PremiseValidationError("There are no valid system properties for the given date. Please contact your administrator."); + + //TODO: sicherstellen, dass die valid days für den zeitpunkt galten zu dem die valid period galt (wenn rückwirkend gerechnet wird) + var validDays = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.VALID_DAYS, set.getId()); + var renewals = period.getRenewals(); + + if(validDays.isEmpty()) + throw new PremiseValidationError("There are no valid days property. Please contact your administrator"); + + var validDaysInt = Integer.parseInt(validDays.get().getCurrentValue()); + + if(!period.getStartDate().plusDays((long) validDaysInt * renewals).isAfter(LocalDateTime.now())) + throw new PremiseValidationError("There are no valid rates for the given date. Please contact your administrator."); } diff --git a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java index 3450b83..af370df 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java @@ -130,8 +130,6 @@ public class ReportTransformer { return new TimePeriod(startDate, endDate); } - ; - private WeightedTotalCosts getWeightedTotalCosts(Map> sectionsMap) { BigDecimal totalPreRunCost = BigDecimal.ZERO; diff --git a/src/main/java/de/avatic/lcc/service/users/AuthorizationService.java b/src/main/java/de/avatic/lcc/service/users/AuthorizationService.java index 5a0e429..58f1cfe 100644 --- a/src/main/java/de/avatic/lcc/service/users/AuthorizationService.java +++ b/src/main/java/de/avatic/lcc/service/users/AuthorizationService.java @@ -9,6 +9,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import java.util.Arrays; + @Service public class AuthorizationService { @@ -35,9 +37,22 @@ public class AuthorizationService { } public boolean isSuper() { + return hasRole("SUPER"); + } + + public boolean hasAnyRole(String... roles) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth.getPrincipal() instanceof LccOidcUser user) { - return user.getAuthorities().stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_SUPER")); + return user.getAuthorities().stream().anyMatch(authority -> Arrays.asList(roles).contains(authority.getAuthority().replace("ROLE_", ""))); + } + + throw new ForbiddenException("No user found"); + } + + public boolean hasRole(String role) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth.getPrincipal() instanceof LccOidcUser user) { + return user.getAuthorities().stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role.toUpperCase())); } throw new ForbiddenException("No user found"); diff --git a/src/main/java/de/avatic/lcc/service/users/UserService.java b/src/main/java/de/avatic/lcc/service/users/UserService.java index 2adedd2..002a04f 100644 --- a/src/main/java/de/avatic/lcc/service/users/UserService.java +++ b/src/main/java/de/avatic/lcc/service/users/UserService.java @@ -49,8 +49,7 @@ public class UserService { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //todo make a service. and simulate user rights in dev profile. - if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser) { - LccOidcUser oidcUser = (LccOidcUser) authentication.getPrincipal(); + if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser oidcUser) { return oidcUser.getAuthorities().stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_SUPER")); } diff --git a/src/main/resources/db/migration/V9__Material.sql b/src/main/resources/db/migration/V10__Material.sql similarity index 100% rename from src/main/resources/db/migration/V9__Material.sql rename to src/main/resources/db/migration/V10__Material.sql diff --git a/src/main/resources/db/migration/V9__Groups.sql b/src/main/resources/db/migration/V9__Groups.sql new file mode 100644 index 0000000..3332df8 --- /dev/null +++ b/src/main/resources/db/migration/V9__Groups.sql @@ -0,0 +1,20 @@ +INSERT INTO sys_group(group_name, group_description) +VALUES ('none', 'no rights'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('basic', 'Login, generate reports'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('calculation', 'Login, generate reports, do calculations'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('freight', 'Login, generate reports, edit freight rates'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('packaging', 'Login, generate reports, edit packaging data'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('material', 'Login, generate reports, edit material data'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('super', + 'Login, generate reports, do calculations, edit freight rates, edit packaging data'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('service', 'Register API Tokens'); +INSERT INTO sys_group(group_name, group_description) +VALUES ('right-management', + 'Add/Remove users, groups, etc.'); \ No newline at end of file diff --git a/src/test/resources/master_data/users.sql b/src/test/resources/master_data/users.sql index 7589c08..57f3f28 100644 --- a/src/test/resources/master_data/users.sql +++ b/src/test/resources/master_data/users.sql @@ -18,6 +18,8 @@ VALUES ('freight', 'Login, generate reports, edit freight rates'); INSERT INTO sys_group(group_name, group_description) VALUES ('packaging', 'Login, generate reports, edit packaging data'); INSERT INTO sys_group(group_name, group_description) +VALUES ('material', 'Login, generate reports, edit material data'); +INSERT INTO sys_group(group_name, group_description) VALUES ('super', 'Login, generate reports, do calculations, edit freight rates, edit packaging data'); INSERT INTO sys_group(group_name, group_description)