diff --git a/src/frontend/src/App.vue b/src/frontend/src/App.vue
index 122f690..38cc276 100644
--- a/src/frontend/src/App.vue
+++ b/src/frontend/src/App.vue
@@ -16,7 +16,6 @@ import ErrorNotification from "@/components/UI/ErrorNotifcation.vue";
export default {
components: {ErrorNotification, TheHeader},
-
}
diff --git a/src/frontend/src/components/layout/TheHeader.vue b/src/frontend/src/components/layout/TheHeader.vue
index 02000f1..6fdb243 100644
--- a/src/frontend/src/components/layout/TheHeader.vue
+++ b/src/frontend/src/components/layout/TheHeader.vue
@@ -3,9 +3,9 @@
@@ -15,10 +15,27 @@
import {PhCube, PhHeart, PhHorse} from "@phosphor-icons/vue";
import NavigationElement from "@/components/UI/NavigationElement.vue";
import Config from "@/pages/Config.vue";
+import {useActiveUserStore} from "@/store/activeuser.js";
+import {mapStores} from "pinia";
export default {
name: "TheHeader",
- components: {Parameterization: Config, NavigationElement, PhCube, PhHeart, PhHorse}
+ components: {Parameterization: Config, NavigationElement, PhCube, PhHeart, PhHorse},
+ computed: {
+ ...mapStores(useActiveUserStore),
+ showCalculation() {
+ return this.activeUserStore.allowCalculation;
+ },
+ showReporting() {
+ return this.activeUserStore.allowReporting;
+ },
+ showConfiguration() {
+ return this.activeUserStore.allowConfiguration;
+ }
+ },
+ async created() {
+ await this.activeUserStore.loadIfRequired();
+ }
}
diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue
index 873f561..8c58801 100644
--- a/src/frontend/src/pages/Config.vue
+++ b/src/frontend/src/pages/Config.vue
@@ -25,6 +25,8 @@ import Rates from "@/components/layout/config/Rates.vue";
import Nodes from "@/components/layout/config/Nodes.vue";
import Materials from "@/components/layout/config/Materials.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
+import {mapStores} from "pinia";
+import {useActiveUserStore} from "@/store/activeuser.js";
export default {
name: "Config",
@@ -32,50 +34,68 @@ export default {
data() {
return {
currentTab: null,
- tabsConfig: [
- {
- title: 'Properties',
- component: markRaw(Properties),
- props: { isSelected: false},
- },
- {
- title: 'System log',
- component: markRaw(ErrorLog),
- props: { isSelected: false},
- },
- {
- title: 'Materials',
- component: markRaw(Materials),
- props: { isSelected: false},
- },
- {
- title: 'Nodes',
- component: markRaw(Nodes),
- props: { isSelected: false},
- },
- {
- title: 'Rates',
- component: markRaw(Rates),
- props: { isSelected: false},
- },
- {
- title: 'Bulk operations',
- component: markRaw(BulkOperations),
- props: { isSelected: false},
- }
- ]
+ propertiesTab: {
+ title: 'Properties',
+ component: markRaw(Properties),
+ props: {isSelected: false},
+ },
+ systemLogTab: {
+ title: 'System log',
+ component: markRaw(ErrorLog),
+ props: {isSelected: false},
+ },
+ materialsTab: {
+ title: 'Materials',
+ component: markRaw(Materials),
+ props: {isSelected: false},
+ },
+ nodesTab: {
+ title: 'Nodes',
+ component: markRaw(Nodes),
+ props: {isSelected: false},
+ },
+ ratesTab: {
+ title: 'Rates',
+ component: markRaw(Rates),
+ props: {isSelected: false},
+ },
+ bulkOperationsTab: {
+ title: 'Bulk operations',
+ component: markRaw(BulkOperations),
+ props: {isSelected: false},
+ }
}
+ },
+ computed: {
+ ...mapStores(useActiveUserStore),
+ tabsConfig() {
+ const tabs = [];
+
+ if(this.activeUserStore.isSuper) {
+ tabs.push(this.propertiesTab);
+ tabs.push(this.systemLogTab);
+ }
+
+ tabs.push(this.materialsTab);
+ tabs.push(this.nodesTab);
+
+ if(this.activeUserStore.allowRates)
+ tabs.push(this.ratesTab);
+
+ tabs.push(this.bulkOperationsTab);
+
+
+ return tabs;
+
+ }
+ },
+ created() {
+
},
methods: {
handleTabChange(eventData) {
-
- console.log("handleTabChange")
-
- const { index, tab } = eventData;
- console.log(`Tab ${index} activated:`, tab.title);
-
- this.tabsConfig.forEach(t => t.props.isSelected = t.title === tab.title);
-
+ const {index, tab} = eventData;
+ this.tabsConfig.forEach(t => t.props.isSelected = t.title === tab.title);
}
}
}
diff --git a/src/frontend/src/pages/NotAuthorized.vue b/src/frontend/src/pages/NotAuthorized.vue
new file mode 100644
index 0000000..58fb8ba
--- /dev/null
+++ b/src/frontend/src/pages/NotAuthorized.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/src/router.js b/src/frontend/src/router.js
index 2d001ac..11d5612 100644
--- a/src/frontend/src/router.js
+++ b/src/frontend/src/router.js
@@ -8,53 +8,118 @@ import CalculationAssistant from "@/pages/CalculationAssistant.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
import CalculationDump from "@/components/layout/dev/CalculationDump.vue";
import DevPage from "@/pages/DevPage.vue";
+import {useActiveUserStore} from "@/store/activeuser.js";
+import NotAuthorized from "@/pages/NotAuthorized.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
+ {
+ path: '/not-authorized',
+ component: NotAuthorized,
+ name: 'not-authorized',
+ },
{
path: '/',
redirect: '/calculations',
name: 'calculation-list',
-
},
{
path: '/assistant',
component: CalculationAssistant,
name: 'assistant',
+ beforeEnter: async (to, from) => {
+
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowCalculation) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/calculations',
component: Calculations,
name: 'home',
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowCalculation) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/edit/:id',
component: CalculationSingleEdit,
name: 'edit',
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowCalculation) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/bulk/:ids',
component: CalculationMassEdit,
name: 'bulk',
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowCalculation) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/bulk/:ids/edit/:id',
component: CalculationSingleEdit,
name: 'bulk-single-edit',
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowCalculation) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/reports',
component: Reporting,
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+
+ if (userStore.allowReporting) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/config',
- component: Config
- },
- {
- path: '/error',
- component: ErrorLog
+ component: Config,
+ beforeEnter: async (to, from) => {
+ const userStore = useActiveUserStore();
+ await userStore.loadIfRequired();
+ if (userStore.allowConfiguration) {
+ return true;
+ }
+ return {name: 'not-authorized'};
+ },
},
{
path: '/dev/dump/:id',
diff --git a/src/frontend/src/store/activeuser.js b/src/frontend/src/store/activeuser.js
new file mode 100644
index 0000000..ad26342
--- /dev/null
+++ b/src/frontend/src/store/activeuser.js
@@ -0,0 +1,59 @@
+import {defineStore} from 'pinia'
+import {config} from '@/config'
+import performRequest from "@/backend.js";
+
+
+export const useActiveUserStore = defineStore('activeUser', {
+ state: () => {
+ return {
+ user: null,
+ }
+ },
+ getters: {
+ allowCalculation(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("super") || state.user.groups?.includes("calculation");
+ },
+ allowConfiguration(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging");
+ },
+ allowReporting(state) {
+ return state.user !== null;
+
+ },
+ isSuper(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("super");
+ },
+ isPackaging(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("packaging");
+ },
+ isFreight(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("freight");
+ },
+ allowRates(state) {
+ if (state.user === null)
+ return false;
+ return state.user.groups?.includes("super") || state.user.groups?.includes("freight");
+ }
+
+ },
+ actions: {
+ async loadIfRequired() {
+ if (this.user === null)
+ await this.load();
+ },
+ async load() {
+ const resp = await performRequest(this, "GET", `${config.backendUrl}/active-user`, null, true);
+ this.user = resp.data;
+ }
+ }
+});
\ No newline at end of file
diff --git a/src/main/java/de/avatic/lcc/config/DevUserEmulationFilter.java b/src/main/java/de/avatic/lcc/config/DevUserEmulationFilter.java
index efd0688..46f6060 100644
--- a/src/main/java/de/avatic/lcc/config/DevUserEmulationFilter.java
+++ b/src/main/java/de/avatic/lcc/config/DevUserEmulationFilter.java
@@ -47,12 +47,12 @@ public class DevUserEmulationFilter extends OncePerRequestFilter {
Integer emulatedUserId = (Integer) session.getAttribute(DEV_USER_ID_SESSION_KEY);
- if(emulatedUserId != null) {
+// if(emulatedUserId != null) {
User user = userRepository.getById(emulatedUserId == null ? 1 : emulatedUserId);
if (user != null) {
setEmulatedUser(user);
}
- }
+// }
filterChain.doFilter(request, response);
}
diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java
index 67e65bb..8d8aa13 100644
--- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java
+++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java
@@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -33,6 +34,7 @@ import java.util.Set;
import java.util.function.Supplier;
@Configuration
+@EnableMethodSecurity
public class SecurityConfig {
@Bean
diff --git a/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java b/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java
index dab14de..627e94b 100644
--- a/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java
+++ b/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java
@@ -81,8 +81,6 @@ public class GlobalExceptionHandler {
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
}
-
-
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity handleConstraintViolation(MethodArgumentTypeMismatchException exception) { //
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 e457ea1..2a6d774 100644
--- a/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java
+++ b/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java
@@ -26,7 +26,6 @@ import java.util.List;
*/
@RestController
@RequestMapping("/api/bulk")
-@RolesAllowed({"ROLE_SUPER", "ROLE_FREIGHT", "ROLE_PACKAGING"})
public class BulkOperationController {
private final BulkOperationService bulkOperationService;
@@ -38,17 +37,20 @@ public class BulkOperationController {
}
@GetMapping({"/status/", "/status"})
+ @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
public ResponseEntity> getBulkStatus() {
return ResponseEntity.ok(bulkOperationService.getStatus());
}
@PostMapping({"/upload/{type}", "/upload/{type}/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
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')")
public ResponseEntity generateTemplate(@PathVariable BulkFileType type) {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=lcc_template_" + type.name().toLowerCase() + ".xlsx");
@@ -62,6 +64,7 @@ public class BulkOperationController {
@GetMapping({"/download/{type}", "/download/{type}/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
public ResponseEntity scheduleDownload(@PathVariable BulkFileType type) {
bulkOperationService.processFileExport(type);
return ResponseEntity.ok().build();
@@ -69,6 +72,7 @@ public class BulkOperationController {
@GetMapping({"/download/{type}/{validity_period_id}", "/download/{type}/{validity_period_id}/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
public ResponseEntity scheduleDownload(@PathVariable BulkFileType type, @PathVariable("validity_period_id") Integer validityPeriodId) {
bulkOperationService.processFileExport(type, validityPeriodId);
return ResponseEntity.ok().build();
@@ -76,6 +80,7 @@ public class BulkOperationController {
}
@GetMapping({"/file/{processId}", "/file/{processId}/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
public ResponseEntity download(@PathVariable("processId") Integer id) {
var op = bulkOperationService.getBulkOperation(id);
diff --git a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java
index b243678..7e26510 100644
--- a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java
+++ b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java
@@ -22,11 +22,13 @@ import de.avatic.lcc.service.calculation.PremiseCreationService;
import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import de.avatic.lcc.util.exception.base.BadRequestException;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -60,6 +62,7 @@ public class PremiseController {
}
@GetMapping({"/view", "/view/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity> listPremises(@RequestParam(required = false) String filter,
@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page,
@@ -78,6 +81,7 @@ public class PremiseController {
}
@GetMapping({"/search", "/search/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity findMaterialsAndSuppliers(@RequestHeader String search) {
try {
@@ -93,6 +97,7 @@ public class PremiseController {
}
@PostMapping({"/create", "/create/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity> createPremises(@RequestBody @Valid CreatePremiseDTO dto) {
if ((dto.getSupplierIds() == null || dto.getSupplierIds().isEmpty()) && (dto.getUserSupplierIds() == null || dto.getUserSupplierIds().isEmpty())) {
@@ -109,23 +114,27 @@ public class PremiseController {
@PostMapping({"/delete", "/delete/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity deletePremises(@RequestParam List premissIds) {
premisesServices.delete(premissIds);
return ResponseEntity.ok().build();
}
@PostMapping({"/archive", "/archive/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity archivePremises(@RequestParam List premissIds) {
premisesServices.archive(premissIds);
return ResponseEntity.ok().build();
}
@GetMapping({"/edit", "/edit/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity> getPremises(@RequestParam List premissIds) {
return ResponseEntity.ok(premisesServices.getPremises(premissIds));
}
@PutMapping({"/start", "/start/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity startCalculation(@RequestBody List premiseIds) {
premisesServices.startCalculation(premiseIds);
return ResponseEntity.ok().build();
@@ -138,45 +147,53 @@ public class PremiseController {
* @return A ResponseEntity with the bulk processing status payload.
*/
@GetMapping({"/status/{processing_id}", "/status/{processing_id}/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity getCalculationStatus(@PathVariable("processing_id") Integer id) {
return ResponseEntity.ok(premisesServices.getCalculationStatus(id));
}
@PostMapping({"/packaging", "/packaging/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity updatePackaging(@RequestBody PackagingUpdateDTO packagingDTO) {
premisesServices.updatePackaging(packagingDTO);
return ResponseEntity.ok().build();
}
@PostMapping({"/material", "/material/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity updateMaterial(@RequestBody @Valid MaterialUpdateDTO materialUpdateDTO) {
premisesServices.updateMaterial(materialUpdateDTO);
return ResponseEntity.ok().build();
}
@PostMapping({"/price", "/price/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity updatePrice(@RequestBody @Valid PriceUpdateDTO priceUpdateDTO) {
premisesServices.updatePrice(priceUpdateDTO);
return ResponseEntity.ok().build();
}
@PostMapping({"/destination", "/destination/"})
+ @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity