diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7dfc609 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,68 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +tab_width = 2 + +[.gitignore] +max_line_length = unset + +[*.md] +max_line_length = unset + +[*.feature] +tab_width = 4 + +[*.gsp] +indent_size = 4 +tab_width = 4 + +[*.haml] +tab_width = 4 + +[*.less] +tab_width = 4 + +[*.styl] +tab_width = 4 + +[.editorconfig] +max_line_length = unset + +[{*.as,*.js2,*.es}] +indent_size = 4 +tab_width = 4 + +[{*.cfml,*.cfm,*.cfc}] +indent_size = 4 +tab_width = 4 + +[{*.cjs,*.js}] +max_line_length = 80 + +[{*.gradle,*.groovy,*.gant,*.gdsl,*.gy,*.gson}] +indent_size = 4 +tab_width = 4 + +[{*.jspx,*.tagx}] +indent_size = 4 +tab_width = 4 + +[{*.kts,*.kt}] +indent_size = 4 +tab_width = 4 + +[{*.vsl,*.vm,*.ft}] +indent_size = 4 +tab_width = 4 + +[{*.xjsp,*.tag,*.jsf,*.jsp,*.jspf,*.tagf}] +indent_size = 4 +tab_width = 4 + +[{*.yml,*.yaml}] +tab_width = 4 + diff --git a/build.gradle b/build.gradle index e3d1f66..b4c7d9b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,13 @@ plugins { id 'java' + id 'eclipse' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' + id 'com.diffplug.eclipse.apt' version '3.26.0' + id 'org.liquibase.gradle' version '3.0.1' } -group = 'com.sms.stockmanagementsystem' +group = 'eu.goldenkoopa' java { toolchain { @@ -24,17 +27,29 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation:3.5.0' + testImplementation 'org.springframework.security:spring-security-test' + compileOnly 'org.projectlombok:lombok' - runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' + + runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'com.googlecode.json-simple:json-simple:1.1.1' implementation 'org.jetbrains:annotations:15.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.3.1' - implementation 'org.hsqldb:hsqldb:2.7.1' implementation 'me.paulschwarz:spring-dotenv:4.0.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + // implementation 'org.liquibase:liquibase-core:4.32.0' + // runtimeOnly 'org.liquibase.ext:liquibase-hibernate6:4.32.0' + +} + +bootRun { + environment 'spring.output.ansi.console-available', true } tasks.register('buildDockerImage') { @@ -60,3 +75,4 @@ tasks.register('buildDockerImage') { tasks.named('test') { useJUnitPlatform() } + diff --git a/dev/docker-compose-dev.yml b/dev/docker-compose-dev.yml index 5ed43d4..f33b01b 100644 --- a/dev/docker-compose-dev.yml +++ b/dev/docker-compose-dev.yml @@ -2,7 +2,7 @@ version: '3.9' services: postgres: - image: postgres:14-alpine + image: postgres:17-alpine ports: - 5432:5432 volumes: @@ -10,4 +10,4 @@ services: environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_DB=${POSTGRES_DB} \ No newline at end of file + - POSTGRES_DB=${POSTGRES_DB} diff --git a/settings.gradle b/settings.gradle index 682509e..43928fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'project' +rootProject.name = 'stockmanagementsystem' diff --git a/src/main/java/com/sms/stockmanagementsystem/project/ContainerController.java b/src/main/java/com/sms/stockmanagementsystem/project/ContainerController.java deleted file mode 100644 index 772cb0e..0000000 --- a/src/main/java/com/sms/stockmanagementsystem/project/ContainerController.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.sms.stockmanagementsystem.project; - -import com.sms.stockmanagementsystem.project.data.Container; -import com.sms.stockmanagementsystem.project.data.Group; -import com.sms.stockmanagementsystem.project.repositories.ContainerRepository; -import com.sms.stockmanagementsystem.project.repositories.GroupRepository; -import org.jetbrains.annotations.NotNull; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.HttpServerErrorException; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/sms/api") -public class ContainerController { - - private final Security security; - - private final ContainerRepository containerRepository; - - private final GroupRepository groupRepository; - - @GetMapping("/health") - public String health() { - return "alive"; - } - - @CrossOrigin - @PostMapping("/container") - public Container setContainerDetails( - @RequestBody String data, - @RequestParam("secret") @NotNull String secret, - @NotNull @RequestParam("server") String serverName) - throws ParseException { - security.checkSecret(secret); - - JSONParser parser = new JSONParser(); - JSONObject jsonObject = (JSONObject) parser.parse(data); - - String name = (String) jsonObject.get("name"); - String user = (String) jsonObject.get("updatedBy"); - String containerData = (String) jsonObject.get("data"); - - Container item; - List containers = containerRepository.findByNameAndServer(name, serverName); - if (containers.isEmpty()) { - item = new Container(name, user, containerData, serverName); - } else { - item = containers.get(0); - item.setData(containerData); - item.setUpdatedBy(user); - item.setUpdatedAt(LocalDateTime.now()); - } - containerRepository.save(item); - - return item; - } - - @CrossOrigin - @GetMapping("/container") - public Container getContainerDetails( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("containerId") String name, - @NotNull @RequestParam("server") String server) { - security.checkSecret(secret); - List item = containerRepository.findByNameAndServer(name, server); - if (item.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST); - } - return item.get(0); - } - - @CrossOrigin - @DeleteMapping("/container") - public String deleteContainerItem( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("containerId") String name, - @NotNull @RequestParam("server") String server) { - security.checkSecret(secret); - List containerList = containerRepository.findByNameAndServer(name, server); - if (containerList.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "container does not exist"); - } - Container container = containerList.get(0); - for (Group group : container.getGroups()) { - group.removeContainer(container); - groupRepository.save(group); - } - containerRepository.delete(container); - return "success"; - } - - @CrossOrigin - @GetMapping("/getContainers") - public List getAllContainers( - @NotNull @RequestParam("secret") String secret, - @RequestParam(value = "server", required = false) String server) { - security.checkSecret(secret); - return server != null - ? containerRepository.findByServer(server) - : containerRepository.findAll(); - } - - @GetMapping("/getContainerGroups") - public List getContainerGroups( - @RequestParam("secret") String secret, - @RequestParam("server") String server, - @RequestParam("containerId") String name) { - security.checkSecret(secret); - List containerList = containerRepository.findByNameAndServer(name, server); - if (containerList.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "container does not exist"); - } - Container container = containerList.get(0); - return container.getGroups(); - } - - @Autowired - public ContainerController(Security security, ContainerRepository containerRepository, GroupRepository groupRepository) { - this.security = security; - this.containerRepository = containerRepository; - this.groupRepository = groupRepository; - } -} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/Security.java b/src/main/java/com/sms/stockmanagementsystem/project/Security.java deleted file mode 100644 index 6287e86..0000000 --- a/src/main/java/com/sms/stockmanagementsystem/project/Security.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sms.stockmanagementsystem.project; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; - -@Service -public class Security { - - Environment environment; - - public void checkSecret(String secret) { - if (!secret.equals(environment.getProperty("secret"))) { - throw new ResponseStatusException(HttpStatus.FORBIDDEN); - } - } - - - @Autowired - public Security(Environment environment) { - this.environment = environment; - } -} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/repositories/ContainerRepository.java b/src/main/java/com/sms/stockmanagementsystem/project/repositories/ContainerRepository.java deleted file mode 100644 index e9d19e9..0000000 --- a/src/main/java/com/sms/stockmanagementsystem/project/repositories/ContainerRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sms.stockmanagementsystem.project.repositories; - -import com.sms.stockmanagementsystem.project.data.Container; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ContainerRepository extends JpaRepository { - - List findByServer(String server); - - List findByNameAndServer(String name, String server); -} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyAuthenticationToken.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyAuthenticationToken.java new file mode 100644 index 0000000..7445476 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyAuthenticationToken.java @@ -0,0 +1,43 @@ +package eu.goldenkoopa.stockmanagementsystem; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import java.util.stream.Collectors; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; + +/** + * ApiKeyAuthenticationToken + */ +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + + private ApiKey apiKey; + + public ApiKeyAuthenticationToken(ApiKey apiKey) { + super(AuthorityUtils.createAuthorityList( + apiKey.getPrivileges() + .stream() + .map(privilege -> privilege.getName()) + .toList())); + this.apiKey = apiKey; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return apiKey; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean authenticated) { + throw new RuntimeException("cannot modify"); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyFilter.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyFilter.java new file mode 100644 index 0000000..f1afc90 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/ApiKeyFilter.java @@ -0,0 +1,52 @@ +package eu.goldenkoopa.stockmanagementsystem; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import eu.goldenkoopa.stockmanagementsystem.services.ApiKeyService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +public class ApiKeyFilter extends OncePerRequestFilter { + + private ApiKeyService apiKeyService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + Map pathVariables = request.getParameterMap(); + if (!pathVariables.keySet().contains("apiKey")) { + filterChain.doFilter(request, response); + return; + } + + ApiKey apiKey; + try { + apiKey = apiKeyService.getApiKeyByKey(pathVariables.get("apiKey")[0]); + } catch (Exception e) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.getWriter().write("api key not valid"); + return; + } + + ApiKeyAuthenticationToken auth = new ApiKeyAuthenticationToken(apiKey); + SecurityContext newContext = SecurityContextHolder.createEmptyContext(); + newContext.setAuthentication(auth); + SecurityContextHolder.setContext(newContext); + + System.out.println("test"); + filterChain.doFilter(request, response); + } + + public ApiKeyFilter(ApiKeyService apiKeyService) { + this.apiKeyService = apiKeyService; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/GlobalExceptionHandler.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/GlobalExceptionHandler.java new file mode 100644 index 0000000..6f8e395 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package eu.goldenkoopa.stockmanagementsystem; + +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + String errorMessage = ex.getBindingResult().getAllErrors().stream() + .map(ObjectError::getDefaultMessage) + .collect(Collectors.joining(", ")); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } +} + diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/SecurityConfig.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/SecurityConfig.java new file mode 100644 index 0000000..72463a1 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/SecurityConfig.java @@ -0,0 +1,48 @@ +package eu.goldenkoopa.stockmanagementsystem; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; + +import eu.goldenkoopa.stockmanagementsystem.services.ApiKeyService; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfig { + + @Autowired + private ApiKeyService apiKeyService; + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) + throws Exception { + http.authorizeHttpRequests((authorize) -> { + // authorize.requestMatchers("/**").permitAll(); + authorize.requestMatchers("/").permitAll(); + authorize.requestMatchers("/error").permitAll(); + authorize.anyRequest().authenticated(); + }) + .formLogin(Customizer.withDefaults()) + .logout(logout -> logout.permitAll()) + .addFilterBefore(new ApiKeyFilter(apiKeyService), AuthorizationFilter.class) + .csrf((csrf) -> csrf.ignoringRequestMatchers("/api/**")) + // .csrf(csrf + // -> csrf.csrfTokenRepository( + // CookieCsrfTokenRepository.withHttpOnlyFalse())); + ; + return http.build(); + } + + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/SetupDataLoader.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/SetupDataLoader.java new file mode 100644 index 0000000..e0b180a --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/SetupDataLoader.java @@ -0,0 +1,128 @@ +package eu.goldenkoopa.stockmanagementsystem; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Privilege; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Role; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.PrivilegeRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.RoleRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.UserRepository; +import eu.goldenkoopa.stockmanagementsystem.utils.AnnotationScanner; +import jakarta.transaction.Transactional; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class SetupDataLoader + implements ApplicationListener { + + private UserRepository userRepository; + + private RoleRepository roleRepository; + + private PrivilegeRepository privilegeRepository; + + PasswordEncoder passwordEncoder; + + boolean alreadySetup = false; + + @Override + @Transactional + public void onApplicationEvent(ContextRefreshedEvent event) { + + if (alreadySetup) { + return; + } + + try { + List privilegeNames = AnnotationScanner.scan(); + privilegeNames.stream() + .filter(privilegeName -> !privilegeName.startsWith("ROLE_")) + .forEach(this::createPrivilegeIfNotFound); + } catch (Exception e) { + System.err.println("failed to load annotations/privileges" + + e.getMessage()); + e.printStackTrace(); + } + + Privilege readPrivilege = createPrivilegeIfNotFound("READ_PRIVILEGE"); + Privilege writePrivilege = createPrivilegeIfNotFound("WRITE_PRIVILEGE"); + + List adminPrivileges = + Arrays.asList(readPrivilege, writePrivilege); + createRoleIfNotFound("ROLE_ADMIN", adminPrivileges); + createRoleIfNotFound("ROLE_STAFF", Arrays.asList(readPrivilege)); + createRoleIfNotFound("ROLE_TEST", Arrays.asList()); + createRoleIfNotFound("ROLE_USER", Arrays.asList(readPrivilege)); + + // TODO: change!!!! + createAdminIfNotFound(); + createUserIfNotFound(); + alreadySetup = true; + } + + private void createAdminIfNotFound() { + if (userRepository.findByUsername("admin").isPresent()) { + return; + } + Role adminRole = roleRepository.findByName("ROLE_ADMIN"); + User user = new User(); + user.setPassword(passwordEncoder.encode("admin")); + user.setUsername("admin"); + user.setRoles(Arrays.asList(adminRole)); + user.setEnabled(true); + userRepository.save(user); + } + + private void createUserIfNotFound() { + if (userRepository.findByUsername("user").isPresent()) { + return; + } + Role adminRole = roleRepository.findByName("ROLE_USER"); + User user = new User(); + user.setPassword(passwordEncoder.encode("test")); + user.setUsername("user"); + user.setRoles(Arrays.asList(adminRole)); + user.setEnabled(true); + userRepository.save(user); + } + + @Transactional + Privilege createPrivilegeIfNotFound(String name) { + + Privilege privilege = privilegeRepository.findByName(name); + if (privilege == null) { + privilege = new Privilege(name); + privilegeRepository.save(privilege); + } + return privilege; + } + + @Transactional + Role createRoleIfNotFound(String name, Collection privileges) { + + Role role = roleRepository.findByName(name); + if (role == null) { + role = new Role(name); + role.setPrivileges(privileges); + roleRepository.save(role); + } + return role; + } + + @Autowired + public SetupDataLoader(UserRepository userRepository, + RoleRepository roleRepository, + PrivilegeRepository privilegeRepository, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.privilegeRepository = privilegeRepository; + this.passwordEncoder = passwordEncoder; + } +} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/StockManagementSystemApplication.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/StockManagementSystemApplication.java similarity index 52% rename from src/main/java/com/sms/stockmanagementsystem/project/StockManagementSystemApplication.java rename to src/main/java/eu/goldenkoopa/stockmanagementsystem/StockManagementSystemApplication.java index 77e5255..68ae4fe 100644 --- a/src/main/java/com/sms/stockmanagementsystem/project/StockManagementSystemApplication.java +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/StockManagementSystemApplication.java @@ -1,10 +1,13 @@ -package com.sms.stockmanagementsystem.project; +package eu.goldenkoopa.stockmanagementsystem; import org.jetbrains.annotations.NotNull; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -28,4 +31,19 @@ public void addCorsMappings(@NotNull CorsRegistry registry) { } }; } + + @Bean + public RoleHierarchy roleHierarchy() { + String hierarchy = "ROLE_ADMIN > ROLE_STAFF \n ROLE_STAFF > ROLE_TEST \n " + + "ROLE_TEST > ROLE_USER"; + RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.fromHierarchy(hierarchy); + return roleHierarchy; + } + + @Bean + public DefaultWebSecurityExpressionHandler customWebSecurityExpressionHandler() { + DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler(); + expressionHandler.setRoleHierarchy(roleHierarchy()); + return expressionHandler; + } } diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ApiKeyController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ApiKeyController.java new file mode 100644 index 0000000..5efdfc8 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ApiKeyController.java @@ -0,0 +1,53 @@ +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; + +import eu.goldenkoopa.stockmanagementsystem.data.dto.request.ApiKeyPostRequestDTO; +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication.ApiKeyWithUserDTO; +import eu.goldenkoopa.stockmanagementsystem.services.ApiKeyService; + +import java.util.List; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** ApiKeyController */ +@RestController +@RequestMapping("/api/v1/apikeys") +@Secured({"ROLE_ADMIN"}) +public class ApiKeyController { + + private final ApiKeyService apiKeyService; + + @GetMapping + public ResponseEntity> getAllApiKeys() { + List apiKeys = + apiKeyService.getAllApiKeys().stream().map(ApiKeyWithUserDTO::from).toList(); + return new ResponseEntity<>(apiKeys, HttpStatus.OK); + } + + @CrossOrigin + @PostMapping + public ResponseEntity generateNewApiKey( + @Valid @RequestBody ApiKeyPostRequestDTO apiKeyRequest, @AuthenticationPrincipal UserDetails userDetails) { + ApiKeyWithUserDTO apiKey = + ApiKeyWithUserDTO.from( + apiKeyService.generateNewApiKey( + apiKeyRequest.privilegeIds(), apiKeyRequest.expirationTime(), userDetails)); + return new ResponseEntity<>(apiKey, HttpStatus.CREATED); + } + + @Autowired + public ApiKeyController(ApiKeyService apiKeyService) { + this.apiKeyService = apiKeyService; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ContainerController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ContainerController.java new file mode 100644 index 0000000..6dc7fea --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/ContainerController.java @@ -0,0 +1,94 @@ +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; + +import eu.goldenkoopa.stockmanagementsystem.data.Container; +import eu.goldenkoopa.stockmanagementsystem.data.Group; +import eu.goldenkoopa.stockmanagementsystem.data.dto.request.ContainerPostRequestDTO; +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.ContainerDTO; +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.GroupDTO; +import eu.goldenkoopa.stockmanagementsystem.services.ContainerService; +import jakarta.validation.Valid; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/containers") +@EnableMethodSecurity(securedEnabled = true) +public class ContainerController { + + private final ContainerService containerService; + + @Secured({"API_HEALTH_READ"}) + @GetMapping("/health") + public String health() { + return "alive"; + } + + @GetMapping() + public List getAllContainers() { + return containerService.getAllContainers().stream().map(ContainerDTO::from).toList(); + } + + @Secured({"WRITE_PRIVILEGE", "API_CONTAINER_CREATE"}) + @PostMapping() + public ResponseEntity setContainerDetails( + @Valid @RequestBody ContainerPostRequestDTO containerDetails, + @AuthenticationPrincipal UserDetails userDetails) { + + return new ResponseEntity( + ContainerDTO.from( + this.containerService.createContainer(containerDetails, userDetails.getUsername())), + HttpStatus.CREATED); + } + + @Secured({"READ_PRIVILEGE", "API_CONTAINER_READ"}) + @GetMapping("/{id}") + public ResponseEntity getContainer( + @PathVariable String id, @RequestParam("server") String server) { + + Container container = this.containerService.getContainer(id, server); + if (container == null) { + return ResponseEntity.notFound().build(); + } + ContainerDTO from = ContainerDTO.from(container); + return ResponseEntity.ok(from); + } + + @Secured({"READ_PRIVILEGE", "API_CONTAINER_DELETE"}) + @DeleteMapping("/{id}") + public ResponseEntity deleteContainer( + @PathVariable String id, @RequestParam("server") String server) { + + containerService.deleteContainer(id, server); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}/groups") + public List getContainerGroups( + @PathVariable String id, @RequestParam("server") String server) { + + List groups = containerService.getContainerGroups(id, server); + + return groups.stream().map(GroupDTO::from).toList(); + } + + + @Autowired + public ContainerController(ContainerService containerService) { + this.containerService = containerService; + } +} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/GroupController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/GroupController.java similarity index 52% rename from src/main/java/com/sms/stockmanagementsystem/project/GroupController.java rename to src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/GroupController.java index 1c959a3..563b9fc 100644 --- a/src/main/java/com/sms/stockmanagementsystem/project/GroupController.java +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/GroupController.java @@ -1,9 +1,11 @@ -package com.sms.stockmanagementsystem.project; +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; -import com.sms.stockmanagementsystem.project.data.Container; -import com.sms.stockmanagementsystem.project.data.Group; -import com.sms.stockmanagementsystem.project.repositories.ContainerRepository; -import com.sms.stockmanagementsystem.project.repositories.GroupRepository; +import eu.goldenkoopa.stockmanagementsystem.data.Container; +import eu.goldenkoopa.stockmanagementsystem.data.Group; +import eu.goldenkoopa.stockmanagementsystem.repositories.ContainerRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.GroupRepository; +import java.util.List; +import java.util.Optional; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -11,27 +13,20 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.server.ResponseStatusException; -import java.util.List; -import java.util.Optional; - @RestController -@RequestMapping("/sms/api") +@RequestMapping("/api/v1") public class GroupController { - private final Security security; - private final GroupRepository groupRepository; private final ContainerRepository containerRepository; @PostMapping("/createGroup") - public String createGroup( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("name") String name, - @RequestParam("user") String user) { - security.checkSecret(secret); + public String createGroup(@NotNull @RequestParam("name") String name, + @RequestParam("user") String user) { if (!groupRepository.findByName(name).isEmpty()) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "group already exists"); + throw new ResponseStatusException(HttpStatus.CONFLICT, + "group already exists"); } Group group = new Group(name, user); groupRepository.save(group); @@ -39,69 +34,65 @@ public String createGroup( } @PostMapping("/addToGroup") - public String addToGroup( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("groupId") Integer groupId, - @NotNull @RequestParam("containerId") String containerId, - @NotNull @RequestParam("server") String server) { - security.checkSecret(secret); + public String + addToGroup(@NotNull @RequestParam("groupId") Integer groupId, + @NotNull @RequestParam("containerId") String containerId, + @NotNull @RequestParam("server") String server) { Optional groupOptional = groupRepository.findById(groupId); if (groupOptional.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "group does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "group does not exist"); } Group group = groupOptional.get(); - List containerOptional = + Optional containerOptional = containerRepository.findByNameAndServer(containerId, server); if (containerOptional.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "id does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "id does not exist"); } - Container container = containerOptional.get(0); + Container container = containerOptional.get(); group.addContainer(container); groupRepository.save(group); return "success"; } @GetMapping("/getGroup") - public List getGroup( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("groupId") Integer groupId, - @RequestParam("server") String server) { - security.checkSecret(secret); + public List + getGroup(@NotNull @RequestParam("groupId") Integer groupId, + @RequestParam("server") String server) { Optional groups = groupRepository.findById(groupId); if (groups.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "group does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "group does not exist"); } return groups.get().getContainers(server); } @DeleteMapping("/deleteGroup") - public String deleteGroup( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("groupId") Integer groupId) { - security.checkSecret(secret); + public String deleteGroup(@NotNull @RequestParam("groupId") Integer groupId) { Optional groupOptional = groupRepository.findById(groupId); if (groupOptional.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "group does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "group does not exist"); } groupRepository.delete(groupOptional.get()); return "success"; } @GetMapping("/groups") - public List getAllGroups(@NotNull @RequestParam("secret") String secret) { - security.checkSecret(secret); + public List + getAllGroups(@NotNull @RequestParam("secret") String secret) { return groupRepository.findAll(); } @PostMapping("/renameGroup") - public String renameGroup( - @NotNull @RequestParam("secret") String secret, - @NotNull @RequestParam("groupId") Integer groupId, - @NotNull @RequestParam("renameTo") String renameString) { - security.checkSecret(secret); + public String + renameGroup(@NotNull @RequestParam("groupId") Integer groupId, + @NotNull @RequestParam("renameTo") String renameString) { Optional groupOptional = groupRepository.findById(groupId); if (groupOptional.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "group does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "group does not exist"); } groupOptional.get().setName(renameString); groupRepository.save(groupOptional.get()); @@ -109,25 +100,26 @@ public String renameGroup( } @DeleteMapping("deleteFromGroup") - public String deleteFromGroup( - @RequestParam("secret") String secret, - @RequestParam("groupId") Integer groupId, - @RequestParam("containerId") String name, - @RequestParam("server") String server) { - security.checkSecret(secret); + public String deleteFromGroup(@RequestParam("groupId") Integer groupId, + @RequestParam("containerId") String name, + @RequestParam("server") String server) { Optional groupOptional = groupRepository.findById(groupId); if (groupOptional.isEmpty()) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "group does not exist"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "group does not exist"); } - List containerOptional = containerRepository.findByNameAndServer(name, server); + Optional containerOptional = + containerRepository.findByNameAndServer(name, server); if (containerOptional.isEmpty()) { throw new HttpServerErrorException( - HttpStatus.BAD_REQUEST, "containerid does not exist (on this server?)"); + HttpStatus.BAD_REQUEST, + "containerid does not exist (on this server?)"); } Group group = groupOptional.get(); - Container container = containerOptional.get(0); + Container container = containerOptional.get(); if (!group.getContainers().contains(container)) { - throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "container is not in group"); + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, + "container is not in group"); } group.removeContainer(container); groupRepository.save(group); @@ -137,8 +129,8 @@ public String deleteFromGroup( } @Autowired - public GroupController(Security security, ContainerRepository containerRepository, GroupRepository groupRepository) { - this.security = security; + public GroupController(ContainerRepository containerRepository, + GroupRepository groupRepository) { this.containerRepository = containerRepository; this.groupRepository = groupRepository; } diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PrivilegeController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PrivilegeController.java new file mode 100644 index 0000000..1c58d5b --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PrivilegeController.java @@ -0,0 +1,40 @@ +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; + +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication.PrivilegeDTO; +import eu.goldenkoopa.stockmanagementsystem.services.PrivilegeService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/privileges") +@Secured({"ROLE_ADMIN"}) +public class PrivilegeController { + + private PrivilegeService privilegeService; + + @GetMapping + public ResponseEntity> getAllPrivileges() { + return ResponseEntity.ok(privilegeService.getAllPrivileges() + .stream() + .map(PrivilegeDTO::from) + .toList()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePrivilege(@PathVariable Long id) { + privilegeService.deletePrivilege(id); + return ResponseEntity.noContent().build(); + } + + @Autowired + public PrivilegeController(PrivilegeService privilegeService) { + this.privilegeService = privilegeService; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PublicUserController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PublicUserController.java new file mode 100644 index 0000000..5dafbf5 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/PublicUserController.java @@ -0,0 +1,42 @@ +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; + +import eu.goldenkoopa.stockmanagementsystem.services.UserService; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +public class PublicUserController { + + private UserService userService; + + @PostMapping("/password") + public ResponseEntity changePassword( + @RequestBody PasswordChangeRequest passwordChangeRequest, + @AuthenticationPrincipal UserDetails userDetails) { + userService.updatePassword( + userDetails.getUsername(), + passwordChangeRequest.oldPassword(), + passwordChangeRequest.newPassword()); + return ResponseEntity.noContent().build(); + } + + private record PasswordChangeRequest( + @NotNull(message = "old password cannot be null") + @NotEmpty(message = "old password cannot be empty") + String oldPassword, + @NotNull(message = "new password cannot be null") + @NotEmpty(message = "new password cannot be empty") + String newPassword) {} + + public PublicUserController(UserService userService) { + this.userService = userService; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/UserController.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/UserController.java new file mode 100644 index 0000000..4fa3b80 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/controllers/v1/UserController.java @@ -0,0 +1,62 @@ +package eu.goldenkoopa.stockmanagementsystem.controllers.v1; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import eu.goldenkoopa.stockmanagementsystem.data.dto.request.UserPostRequestDTO; +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication.UserDTO; +import eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication.UserWithApiKeyDTO; +import eu.goldenkoopa.stockmanagementsystem.services.UserService; + +import java.util.List; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@Secured({"ROLE_ADMIN"}) +public class UserController { + + private UserService userService; + + @GetMapping() + public List getAllUsers() { + return userService.getAllUsers().stream().map(UserWithApiKeyDTO::from).toList(); + } + + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable Long id) { + UserWithApiKeyDTO user = UserWithApiKeyDTO.from(userService.getUserById(id)); + if (user == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(user); + } + + @PostMapping() + public ResponseEntity createUser(@Valid @RequestBody UserPostRequestDTO userPostRequest) { + User user = userService.createUser(userPostRequest.username(), userPostRequest.password()); + return new ResponseEntity(UserDTO.from(user), HttpStatus.CREATED); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + +} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/data/Container.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Container.java similarity index 81% rename from src/main/java/com/sms/stockmanagementsystem/project/data/Container.java rename to src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Container.java index 9ad7b99..6c342b1 100644 --- a/src/main/java/com/sms/stockmanagementsystem/project/data/Container.java +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Container.java @@ -1,15 +1,17 @@ -package com.sms.stockmanagementsystem.project.data; +package eu.goldenkoopa.stockmanagementsystem.data; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Data; - import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; @Data @Entity +@Builder @AllArgsConstructor @Table(name = "Container") public class Container { @@ -34,14 +36,10 @@ public class Container { private String server; - public Container() {} + @Column(columnDefinition = "TEXT") + private String notes; - public Container(String name, LocalDateTime time, String username, String data) { - this.name = name; - this.updatedAt = time; - this.updatedBy = username; - this.data = data; - } + public Container() {} public Container(String name, String user, String data, String server) { this.server = server; @@ -51,6 +49,8 @@ public Container(String name, String user, String data, String server) { this.updatedAt = LocalDateTime.now(); this.createdAt = LocalDateTime.now(); this.createdBy = user; + this.groups = new ArrayList<>(); + this.notes = ""; } public void removeGroup(Group group) { diff --git a/src/main/java/com/sms/stockmanagementsystem/project/data/Group.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Group.java similarity index 95% rename from src/main/java/com/sms/stockmanagementsystem/project/data/Group.java rename to src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Group.java index a0ea279..1f0bf69 100644 --- a/src/main/java/com/sms/stockmanagementsystem/project/data/Group.java +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/Group.java @@ -1,11 +1,10 @@ -package com.sms.stockmanagementsystem.project.data; +package eu.goldenkoopa.stockmanagementsystem.data; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.Data; - import java.time.LocalDateTime; import java.util.List; +import lombok.Data; @Entity @Data diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/ApiKey.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/ApiKey.java new file mode 100644 index 0000000..c0d6a22 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/ApiKey.java @@ -0,0 +1,35 @@ +package eu.goldenkoopa.stockmanagementsystem.data.authentication; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import java.util.List; +import lombok.Data; + +@Entity +@Data +public class ApiKey { + + @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; + + private String key; + + @ManyToOne private User user; + + private LocalDate expirationDate; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "apikeys_privileges", + joinColumns = + @JoinColumn(name = "apikey_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "privilege_id", + referencedColumnName = "id")) + private List privileges; +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Privilege.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Privilege.java new file mode 100644 index 0000000..bdfabdc --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Privilege.java @@ -0,0 +1,21 @@ +package eu.goldenkoopa.stockmanagementsystem.data.authentication; + +import jakarta.persistence.*; +import java.util.Collection; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@Table(name = "privileges") +@NoArgsConstructor +public class Privilege { + + @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; + + private String name; + + @ManyToMany(mappedBy = "privileges") private Collection roles; + + public Privilege(String name) { this.name = name; } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Role.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Role.java new file mode 100644 index 0000000..d3bc45b --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/Role.java @@ -0,0 +1,29 @@ +package eu.goldenkoopa.stockmanagementsystem.data.authentication; + +import jakarta.persistence.*; +import java.util.Collection; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@Table(name = "roles") +@NoArgsConstructor +public class Role { + + @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; + + private String name; + + @ManyToMany(mappedBy = "roles") private Collection users; + + @ManyToMany + @JoinTable( + name = "roles_privileges", + joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), + inverseJoinColumns = + @JoinColumn(name = "privilege_id", referencedColumnName = "id")) + private Collection privileges; + + public Role(String name) { this.name = name; } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/User.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/User.java new file mode 100644 index 0000000..5e5749d --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/authentication/User.java @@ -0,0 +1,43 @@ +package eu.goldenkoopa.stockmanagementsystem.data.authentication; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.Collection; +import lombok.Data; + +@Data +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String username; + private String password; + private boolean enabled; + private boolean tokenExpired; + + @OneToMany(mappedBy = "user") + private Collection apiKeys; + + @ManyToMany + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) + private Collection roles; + + @Override + public String toString() { + return id + username; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ApiKeyPostRequestDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ApiKeyPostRequestDTO.java new file mode 100644 index 0000000..14bae69 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ApiKeyPostRequestDTO.java @@ -0,0 +1,15 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record ApiKeyPostRequestDTO( + @NotNull(message = "Privilege IDs cannot be null") + @NotEmpty(message = "Privilege IDs cannot be empty") + List<@NotNull(message = "Privilege ID cannot be null") Long> privilegeIds, + @NotNull(message = "Expiration time cannot be null") + @NotEmpty(message = "Expiration time cannot be empty") + Long expirationTime) { +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ContainerPostRequestDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ContainerPostRequestDTO.java new file mode 100644 index 0000000..c32200c --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/ContainerPostRequestDTO.java @@ -0,0 +1,9 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ContainerPostRequestDTO( + @NotNull(message = "field 'name' must not be null") String name, + @NotNull(message = "field 'data' must not be null") String data, + @NotNull(message = "field 'server' must not be null") String server) { +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/UserPostRequestDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/UserPostRequestDTO.java new file mode 100644 index 0000000..b889009 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/request/UserPostRequestDTO.java @@ -0,0 +1,11 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record UserPostRequestDTO( + @NotNull(message = "username cannot be null") @NotEmpty(message = "username cannot be empty") + String username, + @NotNull(message = "password cannot be null") @NotEmpty(message = "password cannot be empty") + String password) { +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/ContainerDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/ContainerDTO.java new file mode 100644 index 0000000..a0cdf1f --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/ContainerDTO.java @@ -0,0 +1,33 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response; + +import eu.goldenkoopa.stockmanagementsystem.data.Container; +import eu.goldenkoopa.stockmanagementsystem.data.Group; +import java.time.LocalDateTime; +import java.util.List; + +public record ContainerDTO( + Integer id, + String name, + String createdBy, + String updateBy, + LocalDateTime createdAt, + LocalDateTime updatedAt, + String data, + List groups, + String server, + String notes) { + + public static ContainerDTO from(Container container) { + return new ContainerDTO( + container.getId(), + container.getName(), + container.getCreatedBy(), + container.getUpdatedBy(), + container.getCreatedAt(), + container.getUpdatedAt(), + container.getData(), + container.getGroups().stream().map(Group::getId).toList(), + container.getServer(), + container.getNotes()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/GroupDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/GroupDTO.java new file mode 100644 index 0000000..6fee86d --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/GroupDTO.java @@ -0,0 +1,11 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response; + +import eu.goldenkoopa.stockmanagementsystem.data.Group; +import java.time.LocalDateTime; + +public record GroupDTO(Integer id, LocalDateTime createdAt, String createdBy, String name) { + + public static GroupDTO from(Group group) { + return new GroupDTO(group.getId(), group.getCreatedAt(), group.getCreatedBy(), group.getName()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyDTO.java new file mode 100644 index 0000000..6695637 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyDTO.java @@ -0,0 +1,28 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import java.util.List; + +/** + * A data transfer object (DTO) representing an API key with associated + * privileges. + * This record encapsulates the essential information of an API key, including + * its unique identifier, the key itself, and a list of privileges associated + * with it. + */ +public record ApiKeyDTO(Long id, String key, List privileges) { + + /** + * Creates an ApiKeyDto from an ApiKey entity. + * + * @param apiKey The ApiKey entity to convert + * @return A new ApiKeyDto instance containing the API key's information and + * associated privileges + * @throws NullPointerException if the apiKey parameter is null + */ + public static ApiKeyDTO from(ApiKey apiKey) { + return new ApiKeyDTO( + apiKey.getId(), apiKey.getKey(), + apiKey.getPrivileges().stream().map(PrivilegeDTO::from).toList()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyWithUserDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyWithUserDTO.java new file mode 100644 index 0000000..e37290b --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/ApiKeyWithUserDTO.java @@ -0,0 +1,34 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import java.util.List; + +/** + * A data transfer object (DTO) representing an API key with associated user + * information and privileges. This record encapsulates the essential + * information of an API key, including its unique identifier, the key itself, a + * list of privileges, and the associated user. + */ +public record ApiKeyWithUserDTO(Long id, String key, + List privileges, UserDTO user) { + + /** + * Creates an ApiKeyWithUserDto from an ApiKey entity. + * + * @param apiKey The ApiKey entity to convert + * @return A new ApiKeyWithUserDto instance containing the API key's + * information, privileges, and associated user + * @throws IllegalArgumentException if the apiKey is null, or if the key is + * null + * or blank + * @throws NullPointerException if the privileges or user in the apiKey + * are + * null + */ + public static ApiKeyWithUserDTO from(ApiKey apiKey) { + return new ApiKeyWithUserDTO( + apiKey.getId(), apiKey.getKey(), + apiKey.getPrivileges().stream().map(PrivilegeDTO::from).toList(), + UserDTO.from(apiKey.getUser())); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/PrivilegeDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/PrivilegeDTO.java new file mode 100644 index 0000000..20ee7e5 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/PrivilegeDTO.java @@ -0,0 +1,22 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Privilege; + +/** + * A data transfer object (DTO) representing a privilege. + * This record encapsulates the essential information of a privilege, + * including its unique identifier and name. + */ +public record PrivilegeDTO(Long id, String name) { + + /** + * Creates a PrivilegeDto from a Privilege entity. + * + * @param privilege The Privilege entity to convert + * @return A new PrivilegeDto instance containing the privilege's information + * @throws NullPointerException if the privilege parameter is null + */ + public static PrivilegeDTO from(Privilege privilege) { + return new PrivilegeDTO(privilege.getId(), privilege.getName()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserDTO.java new file mode 100644 index 0000000..ab99218 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserDTO.java @@ -0,0 +1,21 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; + +/** + * A data transfer object (DTO) representing a user. This record encapsulates the essential + * information of a user, including their unique identifier, first name, last name, and username. + */ +public record UserDTO(Long id, String username) { + + /** + * Creates a UserDto from a User entity. + * + * @param user The User entity to convert + * @return A new UserDto instance containing the user's information + * @throws IllegalArgumentException if the user is null or if any required field is null or blank + */ + public static UserDTO from(User user) { + return new UserDTO(user.getId(), user.getUsername()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserWithApiKeyDTO.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserWithApiKeyDTO.java new file mode 100644 index 0000000..9bcf48d --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/data/dto/response/authentication/UserWithApiKeyDTO.java @@ -0,0 +1,23 @@ +package eu.goldenkoopa.stockmanagementsystem.data.dto.response.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import java.util.List; + +/** + * A data transfer object (DTO) representing a user with associated API keys. This record + * encapsulates user information along with a list of their API keys. + */ +public record UserWithApiKeyDTO(Long id, String username, List apiKeys) { + + /** + * Creates a UserWithApiKeyDto from a User entity. + * + * @param user The User entity to convert + * @return A new UserWithApiKeyDto instance containing the user's information and API keys + * @throws NullPointerException if the user parameter is null + */ + public static UserWithApiKeyDTO from(User user) { + return new UserWithApiKeyDTO( + user.getId(), user.getUsername(), user.getApiKeys().stream().map(ApiKeyDTO::from).toList()); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/ContainerRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/ContainerRepository.java new file mode 100644 index 0000000..c0edf14 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/ContainerRepository.java @@ -0,0 +1,20 @@ +package eu.goldenkoopa.stockmanagementsystem.repositories; + +import eu.goldenkoopa.stockmanagementsystem.data.Container; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ContainerRepository extends JpaRepository { + + List findByServer(String server); + + Optional findByNameAndServer(String name, String server); + + boolean existsByNameAndServer(String name, String server); + + void deleteByNameAndServer(String name, String server); +} diff --git a/src/main/java/com/sms/stockmanagementsystem/project/repositories/GroupRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/GroupRepository.java similarity index 63% rename from src/main/java/com/sms/stockmanagementsystem/project/repositories/GroupRepository.java rename to src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/GroupRepository.java index d7f220b..a9e99e6 100644 --- a/src/main/java/com/sms/stockmanagementsystem/project/repositories/GroupRepository.java +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/GroupRepository.java @@ -1,9 +1,8 @@ -package com.sms.stockmanagementsystem.project.repositories; - -import com.sms.stockmanagementsystem.project.data.Group; -import org.springframework.data.jpa.repository.JpaRepository; +package eu.goldenkoopa.stockmanagementsystem.repositories; +import eu.goldenkoopa.stockmanagementsystem.data.Group; import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; public interface GroupRepository extends JpaRepository { diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/ApiKeyRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/ApiKeyRepository.java new file mode 100644 index 0000000..35e9a2d --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/ApiKeyRepository.java @@ -0,0 +1,20 @@ +package eu.goldenkoopa.stockmanagementsystem.repositories.authentication; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + List findByUser(User user); + + boolean existsByKey(String key); + + Optional findByKey(String key); +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/PrivilegeRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/PrivilegeRepository.java new file mode 100644 index 0000000..a548a34 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/PrivilegeRepository.java @@ -0,0 +1,11 @@ +package eu.goldenkoopa.stockmanagementsystem.repositories.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Privilege; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrivilegeRepository extends JpaRepository { + + Privilege findByName(String name); +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/RoleRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/RoleRepository.java new file mode 100644 index 0000000..7fc2cd1 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/RoleRepository.java @@ -0,0 +1,11 @@ +package eu.goldenkoopa.stockmanagementsystem.repositories.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoleRepository extends JpaRepository { + + Role findByName(String name); +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/UserRepository.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/UserRepository.java new file mode 100644 index 0000000..1c16161 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/repositories/authentication/UserRepository.java @@ -0,0 +1,15 @@ +package eu.goldenkoopa.stockmanagementsystem.repositories.authentication; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByUsername(String username); + + List getByUsername(String username); +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ApiKeyService.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ApiKeyService.java new file mode 100644 index 0000000..06ee47e --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ApiKeyService.java @@ -0,0 +1,82 @@ +package eu.goldenkoopa.stockmanagementsystem.services; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.ApiKey; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.ApiKeyRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.PrivilegeRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.UserRepository; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** ApiKeyService */ +@Service +public class ApiKeyService { + + private ApiKeyRepository apiKeyRepository; + private UserRepository userRepository; + private PrivilegeRepository privilegeRepository; + + public ApiKeyService( + ApiKeyRepository apiKeyRepository, + UserRepository userRepository, + PrivilegeRepository privilegeRepository) { + this.apiKeyRepository = apiKeyRepository; + this.userRepository = userRepository; + this.privilegeRepository = privilegeRepository; + } + + /** + * @return list of all api keys + */ + public List getAllApiKeys() { + List apiKeys = apiKeyRepository.findAll(); + return apiKeys; + } + + @Transactional + public ApiKey generateNewApiKey( + List privilegeIds, Long expirationTime, UserDetails userDetails) { + if (privilegeIds == null || privilegeIds.isEmpty()) { + throw new IllegalArgumentException("Privilege IDs cannot be null or empty"); + } + User user = + userRepository + .findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("username not found")); + + ApiKey apiKey = new ApiKey(); + apiKey.setUser(user); + apiKey.setPrivileges( + privilegeIds.stream() + .map( + id -> + privilegeRepository + .findById(id) + .orElseThrow(() -> new RuntimeException("privilege id not found: " + id))) + .toList()); + apiKey.setExpirationDate(LocalDate.now().plusDays(expirationTime)); + apiKey.setKey(generateUniqueApiKey()); + + ApiKey apiKeyResponse = apiKeyRepository.save(apiKey); + return apiKeyResponse; + } + + private String generateUniqueApiKey() { + String key; + do { + key = UUID.randomUUID().toString(); + } while (apiKeyRepository.existsByKey(key)); + return key; + } + + public ApiKey getApiKeyByKey(String string) { + return apiKeyRepository + .findByKey(string) + .orElseThrow(() -> new RuntimeException("api key not found")); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ContainerService.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ContainerService.java new file mode 100644 index 0000000..0c59b44 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/ContainerService.java @@ -0,0 +1,62 @@ +package eu.goldenkoopa.stockmanagementsystem.services; + +import eu.goldenkoopa.stockmanagementsystem.data.Container; +import eu.goldenkoopa.stockmanagementsystem.data.Group; +import eu.goldenkoopa.stockmanagementsystem.data.dto.request.ContainerPostRequestDTO; +import eu.goldenkoopa.stockmanagementsystem.repositories.ContainerRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpServerErrorException; + +@Service +public class ContainerService { + + private ContainerRepository containerRepository; + + public Container createContainer(ContainerPostRequestDTO details, String user) { + if (this.containerRepository.existsByNameAndServer(details.name(), details.server())) { + throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "container does already exist"); + } + + Container container = new Container(details.name(), user, details.data(), details.server()); + return this.containerRepository.save(container); + } + + public Container getContainer(String name, String server) { + return this.containerRepository.findByNameAndServer(name, server).orElse(null); + } + + /** + * Deletes a container from the repository. + * + * @param name The name of the container. + * @param server The server of the container. + */ + public void deleteContainer(String name, String server) { + containerRepository.deleteByNameAndServer(name, server); + } + + @Autowired + public ContainerService(ContainerRepository containerRepository) { + this.containerRepository = containerRepository; + } + + public List getAllContainers() { + return containerRepository.findAll(); + } + + public List getContainerGroups(String name, String server) { + + Optional container = containerRepository.findByNameAndServer(name, server); + + if (container.isEmpty()) { + return new ArrayList(); + } + + return container.get().getGroups(); + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/PrivilegeService.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/PrivilegeService.java new file mode 100644 index 0000000..3e21bb2 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/PrivilegeService.java @@ -0,0 +1,22 @@ +package eu.goldenkoopa.stockmanagementsystem.services; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Privilege; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.PrivilegeRepository; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class PrivilegeService { + + private PrivilegeRepository privilegeRepository; + + public List getAllPrivileges() { + return privilegeRepository.findAll(); + } + + public void deletePrivilege(Long id) { privilegeRepository.deleteById(id); } + + public PrivilegeService(PrivilegeRepository privilegeRepository) { + this.privilegeRepository = privilegeRepository; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/SmsUserDetailsService.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/SmsUserDetailsService.java new file mode 100644 index 0000000..60fc389 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/SmsUserDetailsService.java @@ -0,0 +1,75 @@ +package eu.goldenkoopa.stockmanagementsystem.services; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Privilege; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.Role; +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.RoleRepository; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.UserRepository; +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service("userDetailsService") +@Transactional +public class SmsUserDetailsService implements UserDetailsService { + + @Autowired private UserRepository userRepository; + + @Autowired private RoleRepository roleRepository; + + @Override + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException { + + User user = userRepository.findByUsername(username).orElseThrow( + () -> new UsernameNotFoundException("User not found")); + if (user == null) { + return new org.springframework.security.core.userdetails.User( + " ", " ", true, true, true, true, + getAuthorities( + Arrays.asList(roleRepository.findByName("ROLE_USER")))); + } + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), user.getPassword(), user.isEnabled(), true, true, + true, getAuthorities(user.getRoles())); + } + + private Collection + getAuthorities(Collection roles) { + + return getGrantedAuthorities(getPrivileges(roles)); + } + + private List getPrivileges(Collection roles) { + + List privileges = new ArrayList<>(); + List collection = new ArrayList<>(); + for (Role role : roles) { + privileges.add(role.getName()); + collection.addAll(role.getPrivileges()); + } + for (Privilege item : collection) { + privileges.add(item.getName()); + } + return privileges; + } + + private List + getGrantedAuthorities(List privileges) { + List authorities = new ArrayList<>(); + for (String privilege : privileges) { + authorities.add(new SimpleGrantedAuthority(privilege)); + } + return authorities; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/UserService.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/UserService.java new file mode 100644 index 0000000..9902314 --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/services/UserService.java @@ -0,0 +1,116 @@ +package eu.goldenkoopa.stockmanagementsystem.services; + +import eu.goldenkoopa.stockmanagementsystem.data.authentication.User; +import eu.goldenkoopa.stockmanagementsystem.repositories.authentication.UserRepository; +import java.util.List; +import java.util.regex.Pattern; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +/** UserService */ +@Service +public class UserService { + + private static final String PASSWORD_REGEX = + "^(?=.*[a-z])" // at least one lowercase letter + + "(?=.*[A-Z])" // at least one uppercase letter + + "(?=.*\\d)" // at least one digit + + "(?=.*[^A-Za-z\\d])" // at least one special character (any non-alphanumeric) + + ".{8,}$"; // at least 8 characters + + private static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REGEX); + + UserRepository userRepository; + + PasswordEncoder passwordEncoder; + + /** + * Updates the password for a given user if the current password is correct. + * + * @param username The username of the user. + * @param currentPassword The current password of the user. + * @param newPassword The new password to be set. + * @throws Exception if the current password is incorrect or the user is not found. + */ + public void updatePassword(String username, String currentPassword, String newPassword) { + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new IllegalArgumentException("Current password is incorrect"); + } + + isPasswordStrong(newPassword); + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + } + + /** + * Checks if a user exists by username. + * + * @param username The username to check. + * @return true if the user exists, false otherwise. + */ + public boolean existsByUsername(String username) { + return userRepository.findByUsername(username).isPresent(); + } + + public User getUserById(Long id) { + return userRepository.findById(id).orElseThrow(() -> new RuntimeException("id not found")); + } + + /** + * Saves a user to the repository. + * + * @param user The user to save. + */ + public void saveUser(User user) { + userRepository.save(user); + } + + /** + * Deletes a user from the repository. + * + * @param id The id of the user to delete. + */ + public void deleteUser(Long id) { + userRepository.deleteById(id); + } + + /** Gets all users from the repository. */ + public List getAllUsers() { + return userRepository.findAll(); + } + + public User createUser(String username, String password) { + isPasswordStrong(password); + User user = new User(); + user.setPassword(passwordEncoder.encode(password)); + user.setUsername(username); + user.setEnabled(true); + User returnUser = userRepository.save(user); + return returnUser; + } + + private boolean isPasswordStrong(String password) { + if (password == null || password.isBlank()) { + throw new IllegalArgumentException("Password cannot be empty"); + } + if (!PASSWORD_PATTERN.matcher(password).matches()) { + throw new IllegalArgumentException( + "Password must be at least 8 characters long and contain at least one uppercase letter, " + + "one lowercase letter, one digit, and one special character."); + } + return true; + } + + @Autowired + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + this.userRepository = userRepository; + } +} diff --git a/src/main/java/eu/goldenkoopa/stockmanagementsystem/utils/AnnotationScanner.java b/src/main/java/eu/goldenkoopa/stockmanagementsystem/utils/AnnotationScanner.java new file mode 100644 index 0000000..b23467a --- /dev/null +++ b/src/main/java/eu/goldenkoopa/stockmanagementsystem/utils/AnnotationScanner.java @@ -0,0 +1,69 @@ +package eu.goldenkoopa.stockmanagementsystem.utils; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import org.springframework.security.access.annotation.Secured; + +public class AnnotationScanner { + public static List scan() throws Exception { + String packageName = "eu.goldenkoopa.stockmanagementsystem.controllers.v1"; + List> classes = getClasses(packageName); + List privileges = new ArrayList<>(); + + for (Class clazz : classes) { + if (clazz.isAnnotationPresent(Secured.class)) { + Secured annotation = clazz.getAnnotation(Secured.class); + Arrays.asList(annotation.value()).forEach(privilege -> { + if (!privileges.contains(privilege)) { + privileges.add(privilege); + } + }); + // System.out.println("Class: " + clazz.getName() + + // ", Value: " + Arrays.toString(annotation.value())); + } + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Secured.class)) { + Secured annotation = method.getAnnotation(Secured.class); + Arrays.asList(annotation.value()).forEach(privilege -> { + if (!privileges.contains(privilege)) { + privileges.add(privilege); + } + }); + // System.out.println( + // "Class: " + clazz.getName() + ", Method: " + method.getName() + + // ", Value: " + Arrays.toString(annotation.value())); + } + } + } + System.out.println("Privileges found: " + privileges); + return privileges; + } + + // Method to scan classes from a package + private static List> getClasses(String packageName) + throws IOException, ClassNotFoundException { + List> classes = new ArrayList<>(); + String path = packageName.replace('.', '/'); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = classLoader.getResources(path); + + while (resources.hasMoreElements()) { + File directory = new File(resources.nextElement().getFile()); + if (directory.exists()) { + for (String file : directory.list()) { + if (file.endsWith(".class")) { + String className = packageName + '.' + file.substring(0, file.length() - 6); + classes.add(Class.forName(className)); + } + } + } + } + return classes; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 04b4ef9..e2a342b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,13 +8,22 @@ spring: password: ${POSTGRES_PASSWORD} jpa: hibernate: - ddl-auto: update + ddl-auto: none properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect + liquibase: + enabled: true + change-log: classpath:/db/changelog/db.changelog-master.yml + server: error: include-message: ALWAYS + servlet: + context-path: /sms/ +logging.level: + org.springframework.security: TRACE + org.springframework.web: DEBUG -secret: ${SECRET} \ No newline at end of file +secret: ${SECRET} diff --git a/src/main/resources/db/changelog/db.changelog-master.yml b/src/main/resources/db/changelog/db.changelog-master.yml new file mode 100644 index 0000000..172cddd --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yml @@ -0,0 +1,3 @@ +databaseChangeLog: + - include: + file: db/changelog/changelog-1.yaml