Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
Expand All @@ -15,12 +18,18 @@
@EnableWebSecurity
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Profile("development")
public SecurityFilterChain developmentSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults()) // Enable CSRF protection
.authorizeHttpRequests(auth -> auth
.requestMatchers("/graphiql", "/graphql").permitAll() // Allow requests graphiql
.anyRequest().permitAll() // Allow all requests for development
)
.httpBasic(Customizer.withDefaults()); // Use HTTP Basic authentication for development
Expand Down
23 changes: 17 additions & 6 deletions src/main/java/org/fungover/system2024/user/UserController.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
package org.fungover.system2024.user;

import org.fungover.system2024.user.dto.UserDto;
import org.fungover.system2024.user.dto.UserUpdateDto;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

private final UserService userService;
private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}
public UserController(UserService userService) {
this.userService = userService;
}

@QueryMapping
Iterable<UserDto> users(){ return userService.getAllUsers(); }
@QueryMapping
Iterable<UserDto> users() {
return userService.getAllUsers();
}

@MutationMapping
public Boolean updateUser(@Argument("Input") UserUpdateDto input) {
userService.updateUser(input);
return true;
}
}
68 changes: 54 additions & 14 deletions src/main/java/org/fungover/system2024/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,72 @@
import lombok.extern.slf4j.Slf4j;
import org.fungover.system2024.exception.ResourceNotFoundException;
import org.fungover.system2024.user.dto.UserDto;
import org.fungover.system2024.user.dto.UserUpdateDto;
import org.fungover.system2024.user.entity.User;
import org.fungover.system2024.user.repository.UserRepository;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class UserService {
private final UserRepository userRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.passwordEncoder = new BCryptPasswordEncoder();
}

public Set<UserDto> getAllUsers() {

Set<UserDto> users = userRepository.findAll().stream()
.map(UserDto::from)
.collect(Collectors.toSet());

if (users.isEmpty()) {
log.warn("No users found in database");
throw new ResourceNotFoundException("No users found in database");
}

return users;
}

public Set<UserDto> getAllUsers() {
public void updateUser(UserUpdateDto userUpdateDto) {

Set<UserDto> users = userRepository.findAll().stream()
.map(UserDto::from)
.collect(Collectors.toSet());
User user = getUser();

if (users.isEmpty()) {
log.warn("No users found in database");
throw new ResourceNotFoundException("No users found in database");
if (userUpdateDto.firstName() != null && !userUpdateDto.firstName().isEmpty()) {
user.setFirst_name(userUpdateDto.firstName());
}

if (userUpdateDto.lastName() != null && !userUpdateDto.lastName().isEmpty()) {
user.setLast_name(userUpdateDto.lastName());
}

if (userUpdateDto.password() != null && !userUpdateDto.password().isEmpty()) {
user.setPassword(passwordEncoder.encode(userUpdateDto.password()));
}

userRepository.save(user);
}

return users;
}
private User getUser() {
String currentEmail = getAuthenticatedEmail();
return userRepository.findByEmail(currentEmail)
.orElseThrow(() -> new ResourceNotFoundException("User does not exists"));
}

private static String getAuthenticatedEmail() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
return oAuth2User.getAttribute("email");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.fungover.system2024.user.dto;

public record UserUpdateDto(String firstName, String lastName, String password) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package org.fungover.system2024.user.repository;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.fungover.system2024.user.entity.User;
import org.springframework.data.repository.ListCrudRepository;

import java.util.Optional;

public interface UserRepository extends ListCrudRepository<User, Integer> {
Optional<User> findByEmail(@NotBlank @Email String email);
}
10 changes: 10 additions & 0 deletions src/main/resources/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,13 @@ type User {
name: String!
email: String!
}

type Mutation {
updateUser(Input: UserUpdate!): Boolean
}

input UserUpdate {
firstName: String
lastName: String
password: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.fungover.system2024.user;

import org.fungover.system2024.user.entity.User;
import org.fungover.system2024.user.repository.UserRepository;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.MySQLContainer;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.mockito.Mockito.when;

@SpringBootTest
@AutoConfigureMockMvc
@WithMockUser(username = "development@example.com", roles = "")
class UserControllerSpringBootTest {
static MySQLContainer<?> mysqlContainer = new MySQLContainer<>("mysql:9.1")
.withDatabaseName("system24dbtest")
.withUsername("myuser")
.withPassword("secret");

static {
mysqlContainer.start();
}

@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry propertyRegistry) {
propertyRegistry.add("spring.datasource.url", mysqlContainer::getJdbcUrl);
propertyRegistry.add("spring.datasource.username", mysqlContainer::getUsername);
propertyRegistry.add("spring.datasource.password", mysqlContainer::getPassword);
propertyRegistry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}

@Autowired
MockMvc mockMvc;

@Autowired
private UserRepository userRepository;

private void saveUserToDatabase() {
User user1 = new User();
user1.setFirst_name("Junior");
user1.setLast_name("Senior");
user1.setEmail("development@example.com");
user1.setPassword("secretPassword");
userRepository.save(user1);

User user2 = new User();
user2.setFirst_name("XJunior");
user2.setLast_name("XSenior");
user2.setEmail("xjunior@example.com");
user2.setPassword("hackedPassword");
userRepository.save(user2);
}

private static void authenticateOAuth2User(OAuth2User oAuth2User) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
oAuth2User, null, oAuth2User.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private static @NotNull OAuth2User getoAuth2User(String email) {
OAuth2User oAuth2User = mock(OAuth2User.class);
when(oAuth2User.getAttribute("email")).thenReturn(email);
when(oAuth2User.getAuthorities()).thenReturn(List.of());
return oAuth2User;
}

@Test
void testUpdateUser() throws Exception {
saveUserToDatabase();
final OAuth2User oAuth2User = getoAuth2User("development@example.com");
authenticateOAuth2User(oAuth2User);

String request = "{\"query\":\"mutation {updateUser(Input: {firstName: \\\"Senior\\\", lastName: \\\"SuperSenior\\\", password: \\\"hackedPassword\\\"}) }\"}";

mockMvc.perform(post("/graphql").with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(request))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.updateUser").value(true))
.andReturn();
}

@Test
void testUpdateUserWhenNoAuthority() throws Exception {
final OAuth2User oAuth2User = getoAuth2User("junior@example.com");
authenticateOAuth2User(oAuth2User);

String request = "{\"query\":\"mutation {updateUser(Input: {firstName: \\\"Senior\\\", lastName: \\\"SuperSenior\\\", password: \\\"hackedPassword\\\"}) }\"}";

mockMvc.perform(post("/graphql").with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(request))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.updateUser").doesNotExist())
.andReturn();
}
}
Loading