diff --git a/src/main/java/org/group1/projectbackend/config/DataInitializer.java b/src/main/java/org/group1/projectbackend/config/DataInitializer.java index e27a33e..25e8be7 100644 --- a/src/main/java/org/group1/projectbackend/config/DataInitializer.java +++ b/src/main/java/org/group1/projectbackend/config/DataInitializer.java @@ -2,36 +2,66 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @Component public class DataInitializer implements CommandLineRunner { private final JdbcTemplate jdbcTemplate; + private final PasswordEncoder passwordEncoder; - public DataInitializer(JdbcTemplate jdbcTemplate) { + public DataInitializer(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) { this.jdbcTemplate = jdbcTemplate; + this.passwordEncoder = passwordEncoder; } @Override public void run(String... args) { - Integer userCount = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM users WHERE id = 1", - Integer.class - ); - if (userCount != null && userCount > 0) { - return; - } + // Kryptera lösenord + String userPassword = passwordEncoder.encode("password123"); + String adminPassword = passwordEncoder.encode("admin123"); + // USER jdbcTemplate.update(""" - INSERT INTO users (id, username, email, password, full_name, enabled, created_at) - VALUES - (1, 'user', 'user@example.com', 'password', 'user', true, CURRENT_TIMESTAMP), - (2, 'adamaj01', 'adamaj@example.com', 'password', 'Adam', true, CURRENT_TIMESTAMP), - (3, 'emmtra01', 'emmtra@example.com', 'password', 'Emma', true, CURRENT_TIMESTAMP), - (4, 'erifa101', 'erifa1@example.com', 'password', 'Erika', true, CURRENT_TIMESTAMP), - (5, 'johjan01', 'johjan@example.com', 'password', 'Johan', true, CURRENT_TIMESTAMP) -"""); + INSERT INTO users (id, username, email, password, full_name, enabled, created_at) + VALUES (1, 'testuser', 'testuser@example.com', ?, 'Test User', true, CURRENT_TIMESTAMP) + ON CONFLICT (id) DO NOTHING + """, userPassword); + + // ADMIN USER + jdbcTemplate.update(""" + INSERT INTO users (id, username, email, password, full_name, enabled, created_at) + VALUES (2, 'admin', 'admin@example.com', ?, 'Admin User', true, CURRENT_TIMESTAMP) + ON CONFLICT (id) DO NOTHING + """, adminPassword); + + // ROLLER + jdbcTemplate.update(""" + INSERT INTO roles (id, name) + VALUES (1, 'ROLE_USER') + ON CONFLICT (id) DO NOTHING + """); + + jdbcTemplate.update(""" + INSERT INTO roles (id, name) + VALUES (2, 'ROLE_ADMIN') + ON CONFLICT (id) DO NOTHING + """); + + // Koppla USER → ROLE_USER + jdbcTemplate.update(""" + INSERT INTO user_roles (user_id, role_id) + VALUES (1, 1) + ON CONFLICT DO NOTHING + """); + + // Koppla ADMIN → ROLE_ADMIN + jdbcTemplate.update(""" + INSERT INTO user_roles (user_id, role_id) + VALUES (2, 2) + ON CONFLICT DO NOTHING + """); } -} +} \ No newline at end of file diff --git a/src/main/java/org/group1/projectbackend/controller/ActivityLogController.java b/src/main/java/org/group1/projectbackend/controller/ActivityLogController.java index 797fb70..144fc57 100644 --- a/src/main/java/org/group1/projectbackend/controller/ActivityLogController.java +++ b/src/main/java/org/group1/projectbackend/controller/ActivityLogController.java @@ -19,46 +19,33 @@ public ActivityLogController(ActivityLogService activityLogService) { this.activityLogService = activityLogService; } - // Create activity log @PostMapping public ResponseEntity createActivityLog(@Valid @RequestBody CreateActivityLogDto dto) { - ActivityLogDto createdActivityLog = activityLogService.createActivityLog(dto); - return ResponseEntity.status(201).body(createdActivityLog); + return ResponseEntity.status(201).body(activityLogService.createActivityLog(dto)); } - // Get all activity logs @GetMapping public ResponseEntity> getAllActivityLogs() { - List activityLogs = activityLogService.getAllActivityLogs(); - return ResponseEntity.ok(activityLogs); + return ResponseEntity.ok(activityLogService.getAllActivityLogs()); } - // Get activity log by id @GetMapping("/{activityLogId}") public ResponseEntity getActivityLogById(@PathVariable Long activityLogId) { - ActivityLogDto activityLog = activityLogService.getActivityLogById(activityLogId); - return ResponseEntity.ok(activityLog); + return ResponseEntity.ok(activityLogService.getActivityLogById(activityLogId)); } - // Get activity logs by support ticket id @GetMapping("/ticket/{supportTicketId}") - public ResponseEntity> getActivityLogsBySupportTicketId( + public ResponseEntity> getByTicket( @PathVariable Long supportTicketId, - @RequestParam(defaultValue = "desc") String sortDirection - ) { - return ResponseEntity.ok( - activityLogService.getActivityLogsBySupportTicketId(supportTicketId, sortDirection) - ); + @RequestParam(defaultValue = "desc") String sortDirection) { + List logs = activityLogService.getActivityLogsBySupportTicketId(supportTicketId, sortDirection); + return ResponseEntity.ok(logs); } - // Get activity logs by user id @GetMapping("/user/{userId}") - public ResponseEntity> getActivityLogsByUserId( + public ResponseEntity> getByUser( @PathVariable Long userId, - @RequestParam(defaultValue = "asc") String sortDirection - ) { - return ResponseEntity.ok( - activityLogService.getActivityLogsByUserId(userId, sortDirection) - ); + @RequestParam(defaultValue = "asc") String sortDirection) { + return ResponseEntity.ok(activityLogService.getActivityLogsByUserId(userId, sortDirection)); } } diff --git a/src/main/java/org/group1/projectbackend/dto/ApiResponse.java b/src/main/java/org/group1/projectbackend/dto/ApiResponse.java new file mode 100644 index 0000000..5dc4dd0 --- /dev/null +++ b/src/main/java/org/group1/projectbackend/dto/ApiResponse.java @@ -0,0 +1,20 @@ +package org.group1.projectbackend.dto; + +import java.time.LocalDateTime; + +public class ApiResponse { + + private String status; + private T data; + private LocalDateTime timestamp; + + public ApiResponse(String status, T data) { + this.status = status; + this.data = data; + this.timestamp = LocalDateTime.now(); + } + + public String getStatus() { return status; } + public T getData() { return data; } + public LocalDateTime getTimestamp() { return timestamp; } +} \ No newline at end of file diff --git a/src/main/java/org/group1/projectbackend/security/CustomUserDetailsService.java b/src/main/java/org/group1/projectbackend/security/CustomUserDetailsService.java new file mode 100644 index 0000000..bc649ca --- /dev/null +++ b/src/main/java/org/group1/projectbackend/security/CustomUserDetailsService.java @@ -0,0 +1,33 @@ +package org.group1.projectbackend.security; + +import org.group1.projectbackend.entity.User; +import org.group1.projectbackend.repository.UserRepository; +import org.springframework.security.core.userdetails.*; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .roles( + user.getRoles().stream() + .map(role -> role.getName().name().replace("ROLE_", "")) + .toArray(String[]::new) + ) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/group1/projectbackend/security/SecurityConfig.java b/src/main/java/org/group1/projectbackend/security/SecurityConfig.java index 14d4518..b28a0d8 100644 --- a/src/main/java/org/group1/projectbackend/security/SecurityConfig.java +++ b/src/main/java/org/group1/projectbackend/security/SecurityConfig.java @@ -2,34 +2,43 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { + private final CustomUserDetailsService userDetailsService; + + public SecurityConfig(CustomUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http - .csrf(csrf -> csrf.disable()) - .httpBasic(Customizer.withDefaults()) + .formLogin(form -> form + .defaultSuccessUrl("/", true) + .permitAll() + ) .authorizeHttpRequests(auth -> auth + .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll() + .requestMatchers("/activitylogs/**").hasRole("ADMIN") + .requestMatchers("/comments/**").authenticated() + .requestMatchers("/api/documents/**").authenticated() + .requestMatchers("/api/tickets/**").authenticated() + .requestMatchers("/documents/**").authenticated() + .requestMatchers("/tickets/**").authenticated() .anyRequest().authenticated() ) + .userDetailsService(userDetailsService) .build(); } @Bean - public UserDetailsService userDetailsService() { - return new InMemoryUserDetailsManager( - User.withUsername("user") - .password("{noop}password") - .roles("USER") - .build() - ); + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); } } diff --git a/src/main/java/org/group1/projectbackend/service/ActivityLogService.java b/src/main/java/org/group1/projectbackend/service/ActivityLogService.java index b940fad..6d8dbfa 100644 --- a/src/main/java/org/group1/projectbackend/service/ActivityLogService.java +++ b/src/main/java/org/group1/projectbackend/service/ActivityLogService.java @@ -66,7 +66,9 @@ public ActivityLogDto getActivityLogById(Long id) { return activityLogMapper.toDto(activityLog); } + public List getActivityLogsBySupportTicketId(Long supportTicketId, String sortDirection) { + if (!supportTicketRepository.existsById(supportTicketId)) { throw new ResourceNotFoundException("Support ticket not found with id: " + supportTicketId); } @@ -91,4 +93,4 @@ public List getActivityLogsByUserId(Long userId, String sortDire .map(activityLogMapper::toDto) .toList(); } -} +} \ No newline at end of file diff --git a/src/test/java/org/group1/projectbackend/controller/DocumentControllerTest.java b/src/test/java/org/group1/projectbackend/controller/DocumentControllerTest.java index 917a507..934b411 100644 --- a/src/test/java/org/group1/projectbackend/controller/DocumentControllerTest.java +++ b/src/test/java/org/group1/projectbackend/controller/DocumentControllerTest.java @@ -85,6 +85,22 @@ void shouldUploadDocumentForTicket() throws Exception { .andExpect(jsonPath("$.uploadedById").value(1)); } + @Test + void shouldReturnUnauthorizedWhenUploadingDocumentWithoutAuthentication() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "setup-guide.pdf", + "application/pdf", + "file-content".getBytes() + ); + + mockMvc.perform(multipart("/api/tickets/10/documents") + .file(file) + .param("uploadedByUserId", "1") + .with(csrf())) + .andExpect(status().isUnauthorized()); + } + @Test @WithMockUser void shouldReturnBadRequestWhenUploadedFileIsEmpty() throws Exception { diff --git a/src/test/java/org/group1/projectbackend/controller/SupportTicketControllerTest.java b/src/test/java/org/group1/projectbackend/controller/SupportTicketControllerTest.java index 9e122c4..7e9c3e5 100644 --- a/src/test/java/org/group1/projectbackend/controller/SupportTicketControllerTest.java +++ b/src/test/java/org/group1/projectbackend/controller/SupportTicketControllerTest.java @@ -74,6 +74,21 @@ void shouldCreateTicket() throws Exception { .andExpect(jsonPath("$.title").value("VPN access issue")); } + @Test + void shouldReturnUnauthorizedWhenCreatingTicketWithoutAuthentication() throws Exception { + mockMvc.perform(post("/api/tickets") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "title": "VPN access issue", + "description": "Cannot connect to the company VPN from home.", + "priority": "HIGH" + } + """)) + .andExpect(status().isUnauthorized()); + } + @Test @WithMockUser void shouldGetTicketById() throws Exception {