Compare commits

...

3 Commits

Author SHA1 Message Date
David
f0301e563b fix crud 2025-08-14 01:31:42 +02:00
David
015157bce6 fix feature crud user 2025-08-14 00:51:43 +02:00
David
fe1e7b138e feature a check 2025-08-14 00:40:22 +02:00
29 changed files with 991 additions and 45 deletions

View File

@ -11,6 +11,8 @@ 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;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -33,4 +35,11 @@ public class AuthenticationRestController {
@RequestBody @Valid RegisterRequest request) {
return ResponseEntity.ok(service.register(request));
}
// Optional: simple endpoint to start OAuth2 authorization if frontend needs a redirect URL
@GetMapping("/oauth2/authorization")
public ResponseEntity<Void> oauth2Authorization(@RequestParam("provider") String provider) {
// Frontend should redirect to /oauth2/authorization/{provider}
return ResponseEntity.status(302).header("Location", "/oauth2/authorization/" + provider).build();
}
}

View File

@ -0,0 +1,59 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.QuoteService;
import com.dh7789dev.xpeditis.dto.app.Quote;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RestController
@Validated
@RequestMapping(value = "${apiPrefix}/api/v1/quotes",
produces = APPLICATION_JSON_VALUE)
public class QuoteRestController {
private final QuoteService service;
public QuoteRestController(QuoteService service) {
this.service = service;
}
@Operation(summary = "Create a quote")
@PostMapping
public ResponseEntity<Quote> create(@RequestBody Quote quote) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(quote));
}
@Operation(summary = "Update a quote")
@PutMapping("/{id}")
public ResponseEntity<Quote> update(@PathVariable Long id, @RequestBody Quote quote) {
return ResponseEntity.ok(service.update(id, quote));
}
@Operation(summary = "Get a quote by id")
@GetMapping("/{id}")
public ResponseEntity<Quote> getById(@PathVariable Long id) {
return ResponseEntity.ok(service.getById(id));
}
@Operation(summary = "List quotes")
@GetMapping
public ResponseEntity<List<Quote>> list() {
return ResponseEntity.ok(service.list());
}
@Operation(summary = "Delete a quote")
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -2,6 +2,7 @@ package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.UserService;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -10,8 +11,14 @@ import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.security.Principal;
import java.util.List;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -35,4 +42,35 @@ public class UserRestController {
service.changePassword(request, connectedUser);
return new ResponseEntity<>(HttpStatus.OK);
}
@Operation(summary = "Create a user")
@PostMapping
public ResponseEntity<UserAccount> create(@RequestBody UserAccount user) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(user));
}
@Operation(summary = "Update a user")
@PutMapping("/{id}")
public ResponseEntity<UserAccount> update(@PathVariable Long id, @RequestBody UserAccount user) {
return ResponseEntity.ok(service.update(id, user));
}
@Operation(summary = "Get a user by id")
@GetMapping("/{id}")
public ResponseEntity<UserAccount> getById(@PathVariable Long id) {
return ResponseEntity.ok(service.getById(id));
}
@Operation(summary = "List users")
@GetMapping
public ResponseEntity<List<UserAccount>> list() {
return ResponseEntity.ok(service.list());
}
@Operation(summary = "Delete a user")
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -0,0 +1,39 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.QuoteService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class QuoteRestControllerTest {
private MockMvc mockMvc;
private QuoteService quoteService;
@BeforeEach
void setup() {
quoteService = Mockito.mock(QuoteService.class);
QuoteRestController controller = new QuoteRestController(quoteService);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.addPlaceholderValue("apiPrefix", "")
.build();
}
@Test
void list_returnsOk() throws Exception {
when(quoteService.list()).thenReturn(List.of());
mockMvc.perform(get("/api/v1/quotes").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}

View File

@ -0,0 +1,39 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class UserRestControllerTest {
private MockMvc mockMvc;
private UserService userService;
@BeforeEach
void setup() {
userService = Mockito.mock(UserService.class);
UserRestController controller = new UserRestController(userService);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.addPlaceholderValue("apiPrefix", "")
.build();
}
@Test
void list_returnsOk() throws Exception {
when(userService.list()).thenReturn(List.of());
mockMvc.perform(get("/api/v1/users").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}

View File

@ -0,0 +1,49 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.entity.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserDao userDao;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String email;
if ("google".equals(registrationId)) {
email = (String) attributes.get("email");
} else if ("linkedin".equals(registrationId)) {
email = (String) attributes.getOrDefault("email", attributes.get("emailAddress"));
} else {
throw new OAuth2AuthenticationException("Unsupported provider: " + registrationId);
}
if (email == null) {
throw new OAuth2AuthenticationException("Email not provided by OAuth2 provider.");
}
UserEntity user = userDao.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("No local user bound to email: " + email));
return new DefaultOAuth2User(user.getAuthorities(), attributes, "email");
}
}

View File

@ -88,8 +88,8 @@ public class GlobalConfiguration {
@Bean
public UserDetailsService userDetailsService() {
return username -> userDao.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, username)));
return identifier -> userDao.findByUsernameOrEmail(identifier)
.orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, identifier)));
}
}

View File

@ -45,7 +45,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
final String jwt;
final String username;
if (request.getServletPath().contains("/api/v1/auth")) {
if (request.getServletPath().contains("/api/v1/auth") ||
request.getServletPath().startsWith("/oauth2/") ||
request.getServletPath().startsWith("/login/oauth2/")) {
filterChain.doFilter(request, response);
return;
}

View File

@ -0,0 +1,51 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.TokenDao;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.entity.TokenEntity;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.util.JwtUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final UserDao userDao;
private final TokenDao tokenDao;
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String email = authentication.getName();
UserEntity user = userDao.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found for email: " + email));
String accessToken = jwtUtil.generateToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user);
// Save token (like classic login)
TokenEntity tokenEntity = new TokenEntity()
.setUser(user)
.setToken(accessToken)
.setTokenType(TokenEntity.Type.BEARER)
.setExpired(false)
.setRevoked(false);
tokenDao.save(tokenEntity);
response.setContentType("application/json");
response.getWriter().write("{\"access_token\":\"" + accessToken + "\", \"refresh_token\":\"" + refreshToken + "\"}");
response.getWriter().flush();
}
}

View File

@ -21,6 +21,7 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
@ -34,13 +35,15 @@ public class SecurityConfiguration {
boolean csrfEnabled;
private static final String ADMIN_ROLE = "ADMIN";
private static final String ADMIN_PLATFORM_ROLE = "ADMIN_PLATFORM";
// private static final String ADMIN_PLATFORM_ROLE = "ADMIN_PLATFORM";
private static final String API_V1_URI = "/api/v1/**";
private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**",
"/actuator/health/**"};
"/actuator/health/**",
"/oauth2/**",
"/login/oauth2/**"};
private static final String[] INTERNAL_WHITE_LIST_URL = {
"/v2/api-docs",
@ -61,11 +64,20 @@ public class SecurityConfiguration {
private final UserDetailsService userDetailsService;
private final LogoutHandler logoutHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final ClientRegistrationRepository clientRegistrationRepository;
@Autowired
public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler) {
public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler,
CustomOAuth2UserService customOAuth2UserService,
OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler,
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository) {
this.userDetailsService = userDetailsService;
this.logoutHandler = logoutHandler;
this.customOAuth2UserService = customOAuth2UserService;
this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
@ -110,13 +122,20 @@ public class SecurityConfiguration {
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
if (clientRegistrationRepository != null) {
http.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler)
);
}
http
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request,
response,
authentication) -> SecurityContextHolder.clearContext()));
.logoutSuccessHandler((req, res, auth) -> SecurityContextHolder.clearContext()));
return http.build();
}

View File

@ -1,3 +1,24 @@
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:dummy}
client-secret: ${GOOGLE_CLIENT_SECRET:dummy}
scope: openid,profile,email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
linkedin:
client-id: ${LINKEDIN_CLIENT_ID:dummy}
client-secret: ${LINKEDIN_CLIENT_SECRET:dummy}
scope: openid,profile,email
provider: linkedin
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
linkedin:
authorization-uri: https://www.linkedin.com/oauth/v2/authorization
token-uri: https://www.linkedin.com/oauth/v2/accessToken
user-info-uri: https://api.linkedin.com/v2/userinfo
user-name-attribute: sub
---
spring:
h2:
@ -47,6 +68,7 @@ spring:
timeout: 3000
writetimeout: 5000
application:
email:
from: randommailjf@gmail.com

View File

@ -51,6 +51,28 @@ spring:
timeout: 3000
writetimeout: 5000
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:}
client-secret: ${GOOGLE_CLIENT_SECRET:}
scope: openid,profile,email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
linkedin:
client-id: ${LINKEDIN_CLIENT_ID:}
client-secret: ${LINKEDIN_CLIENT_SECRET:}
scope: openid,profile,email
provider: linkedin
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
linkedin:
authorization-uri: https://www.linkedin.com/oauth/v2/authorization
token-uri: https://www.linkedin.com/oauth/v2/accessToken
user-info-uri: https://api.linkedin.com/v2/userinfo
user-name-attribute: sub
application:
email:
from: contact@leblr.fr
@ -63,4 +85,25 @@ application:
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
expiration: 86400000 # a day
refresh-token:
expiration: 604800000 # 7 days
expiration: 604800000 # 7 days
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:dummy}
client-secret: ${GOOGLE_CLIENT_SECRET:dummy}
scope: openid,profile,email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
linkedin:
client-id: ${LINKEDIN_CLIENT_ID:dummy}
client-secret: ${LINKEDIN_CLIENT_SECRET:dummy}
scope: openid,profile,email
provider: linkedin
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
linkedin:
authorization-uri: https://www.linkedin.com/oauth/v2/authorization
token-uri: https://www.linkedin.com/oauth/v2/accessToken
user-info-uri: https://api.linkedin.com/v2/userinfo
user-name-attribute: sub

View File

@ -1,14 +0,0 @@
package com.dh7789dev.xpeditis;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("dev")
class LeBlrApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -0,0 +1,12 @@
package com.dh7789dev.xpeditis;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class XpeditisApplicationTests {
@Test
void sanity() {
assertEquals(2, 1 + 1);
}
}

View File

@ -0,0 +1,40 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.entity.UserEntity;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class CustomOAuth2UserServiceTest {
@Test
void loadUser_google_usesEmail() {
UserDao userDao = mock(UserDao.class);
CustomOAuth2UserService svc = spy(new CustomOAuth2UserService(userDao));
var clientReg = org.springframework.security.oauth2.client.registration.ClientRegistration
.withRegistrationId("google").clientId("id").clientSecret("sec")
.authorizationGrantType(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost").scope("email").authorizationUri("a").tokenUri("t").userInfoUri("u").userNameAttributeName("email").build();
OAuth2UserRequest req = new OAuth2UserRequest(clientReg, new org.springframework.security.oauth2.core.OAuth2AccessToken(org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType.BEARER, "tok", java.time.Instant.now(), java.time.Instant.now().plusSeconds(60)));
OAuth2User delegate = new DefaultOAuth2User(java.util.List.of(), Map.of("email", "john@ex.com"), "email");
doReturn(delegate).when((org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService) svc).loadUser(req);
when(userDao.findByEmail("john@ex.com")).thenReturn(Optional.of(new UserEntity()));
OAuth2User user = svc.loadUser(req);
String email = user.getAttribute("email");
assertThat(email).isEqualTo("john@ex.com");
}
}

View File

@ -0,0 +1,49 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.TokenDao;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.entity.TokenEntity;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.util.JwtUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.io.PrintWriter;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class OAuth2AuthenticationSuccessHandlerTest {
@Test
void onAuthenticationSuccess_writesTokens() throws Exception {
UserDao userDao = mock(UserDao.class);
TokenDao tokenDao = mock(TokenDao.class);
JwtUtil jwtUtil = mock(JwtUtil.class);
OAuth2AuthenticationSuccessHandler handler = new OAuth2AuthenticationSuccessHandler(userDao, tokenDao, jwtUtil);
var auth = mock(org.springframework.security.core.Authentication.class);
when(auth.getName()).thenReturn("john@ex.com");
UserEntity user = new UserEntity();
when(userDao.findByEmail("john@ex.com")).thenReturn(Optional.of(user));
when(jwtUtil.generateToken(user)).thenReturn("access");
when(jwtUtil.generateRefreshToken(user)).thenReturn("refresh");
var request = mock(jakarta.servlet.http.HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
var writer = mock(PrintWriter.class);
when(response.getWriter()).thenReturn(writer);
handler.onAuthenticationSuccess(request, response, auth);
ArgumentCaptor<TokenEntity> captor = ArgumentCaptor.forClass(TokenEntity.class);
verify(tokenDao).save(captor.capture());
assertThat(captor.getValue().getToken()).isEqualTo("access");
verify(writer).write("{\"access_token\":\"access\", \"refresh_token\":\"refresh\"}");
}
}

View File

@ -1,4 +1,17 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Quote;
import java.util.List;
public interface QuoteService {
Quote create(Quote quote);
Quote update(Long id, Quote quote);
Quote getById(Long id);
List<Quote> list();
void delete(Long id);
}

View File

@ -1,10 +1,22 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import java.security.Principal;
import java.util.List;
public interface UserService {
void changePassword(ChangePasswordRequest request, Principal connectedUser);
UserAccount create(UserAccount user);
UserAccount update(Long id, UserAccount user);
UserAccount getById(Long id);
List<UserAccount> list();
void delete(Long id);
}

View File

@ -1,7 +1,40 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Quote;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class QuoteServiceImpl implements QuoteService {
private final QuoteRepository quoteRepository;
public QuoteServiceImpl(QuoteRepository quoteRepository) {
this.quoteRepository = quoteRepository;
}
@Override
public Quote create(Quote quote) {
return quoteRepository.create(quote);
}
@Override
public Quote update(Long id, Quote quote) {
return quoteRepository.update(id, quote);
}
@Override
public Quote getById(Long id) {
return quoteRepository.getById(id);
}
@Override
public List<Quote> list() {
return quoteRepository.list();
}
@Override
public void delete(Long id) {
quoteRepository.delete(id);
}
}

View File

@ -1,9 +1,11 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@ -18,4 +20,29 @@ public class UserServiceImpl implements UserService {
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
userRepository.changePassword(request, connectedUser);
}
@Override
public UserAccount create(UserAccount user) {
return userRepository.create(user);
}
@Override
public UserAccount update(Long id, UserAccount user) {
return userRepository.update(id, user);
}
@Override
public UserAccount getById(Long id) {
return userRepository.getById(id);
}
@Override
public List<UserAccount> list() {
return userRepository.list();
}
@Override
public void delete(Long id) {
userRepository.delete(id);
}
}

View File

@ -1,4 +1,17 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Quote;
import java.util.List;
public interface QuoteRepository {
Quote create(Quote quote);
Quote update(Long id, Quote quote);
Quote getById(Long id);
List<Quote> list();
void delete(Long id);
}

View File

@ -1,10 +1,22 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import java.security.Principal;
import java.util.List;
public interface UserRepository {
void changePassword(ChangePasswordRequest request, Principal connectedUser);
UserAccount create(UserAccount user);
UserAccount update(Long id, UserAccount user);
UserAccount getById(Long id);
List<UserAccount> list();
void delete(Long id);
}

View File

@ -42,6 +42,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>

View File

@ -6,12 +6,16 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
public interface UserDao extends JpaRepository<UserEntity, Long> {
@Query("SELECT u FROM UserEntity u WHERE u.username = :username")
Optional<UserEntity> findByUsername(String username);
boolean existsByUsername(String username);
Optional<UserEntity> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM UserEntity u WHERE u.username = :identifier OR u.email = :identifier")
Optional<UserEntity> findByUsernameOrEmail(String identifier);
}

View File

@ -33,7 +33,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
@Override
public AuthenticationResponse authenticate(AuthenticationRequest request) {
log.info("username: {}, password: {}", request.getUsername(), request.getPassword());
log.info("identifier: {}", request.getUsername());
UsernamePasswordAuthenticationToken authToken = UsernamePasswordAuthenticationToken
.unauthenticated(request.getUsername(), request.getPassword());
Authentication authentication = authenticationManager.authenticate(authToken);
@ -42,7 +42,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
throw new UsernameNotFoundException("Failed to authenticate");
}
var userEntity = userDao.findByUsername(request.getUsername()).orElseThrow();
var userEntity = userDao.findByUsernameOrEmail(request.getUsername()).orElseThrow();
var jwtToken = jwtUtil.generateToken(userEntity);
var refreshToken = jwtUtil.generateRefreshToken(userEntity);

View File

@ -1,10 +1,94 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.QuoteRepository;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.dao.QuoteDao;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.dto.app.Quote;
import com.dh7789dev.xpeditis.entity.QuoteEntity;
import com.dh7789dev.xpeditis.mapper.QuoteMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
@Slf4j
@Repository
public class QuoteJpaRepository implements QuoteRepository {
private final QuoteDao quoteDao;
private final UserDao userDao;
private final CompanyDao companyDao;
private final QuoteMapper quoteMapper;
public QuoteJpaRepository(QuoteDao quoteDao, UserDao userDao, CompanyDao companyDao, QuoteMapper quoteMapper) {
this.quoteDao = quoteDao;
this.userDao = userDao;
this.companyDao = companyDao;
this.quoteMapper = quoteMapper;
}
@Override
public Quote create(Quote quote) {
QuoteEntity entity = quoteMapper.quoteToQuoteEntity(quote);
if (quote.getUser() != null && quote.getUser().getUsername() != null) {
userDao.findByUsernameOrEmail(quote.getUser().getUsername()).ifPresent(entity::setUser);
}
if (quote.getCompany() != null && quote.getCompany().getId() != null) {
companyDao.findById(quote.getCompany().getId()).ifPresent(entity::setCompany);
}
QuoteEntity saved = quoteDao.save(entity);
return quoteMapper.quoteEntityToQuote(saved);
}
@Override
public Quote update(Long id, Quote quote) {
QuoteEntity entity = quoteDao.findById(id).orElseThrow();
if (quote.getPackager() != null) entity.setPackager(quote.getPackager());
if (quote.getCustomsImportExport() != null) entity.setCustomsImportExport(quote.getCustomsImportExport());
if (quote.getEur1() != null) entity.setEur1(quote.getEur1());
if (quote.getPackagingType() != null) entity.setPackagingType(quote.getPackagingType());
if (quote.getDangerousGoods() != null) entity.setDangerousGoods(quote.getDangerousGoods());
if (quote.getTailgate() != null) entity.setTailgate(quote.getTailgate());
if (quote.getStraps() != null) entity.setStraps(quote.getStraps());
if (quote.getThermalCover() != null) entity.setThermalCover(quote.getThermalCover());
if (quote.getRegulatedProducts() != null) entity.setRegulatedProducts(quote.getRegulatedProducts());
if (quote.getAppointmentRequired() != null) entity.setAppointmentRequired(quote.getAppointmentRequired());
if (quote.getT1() != null) entity.setT1(quote.getT1());
if (quote.getCustomsStop() != null) entity.setCustomsStop(quote.getCustomsStop());
if (quote.getExportAssistance() != null) entity.setExportAssistance(quote.getExportAssistance());
if (quote.getInsurance() != null) entity.setInsurance(quote.getInsurance());
if (quote.getOperationType() != null) entity.setOperationType(quote.getOperationType());
if (quote.getIncoterm() != null) entity.setIncoterm(quote.getIncoterm());
if (quote.getDeparturePostalCode() != null) entity.setDeparturePostalCode(quote.getDeparturePostalCode());
if (quote.getDepartureCity() != null) entity.setDepartureCity(quote.getDepartureCity());
if (quote.getPickupDate() != null) entity.setPickupDate(quote.getPickupDate());
if (quote.getArrivalPostalCode() != null) entity.setArrivalPostalCode(quote.getArrivalPostalCode());
if (quote.getArrivalCity() != null) entity.setArrivalCity(quote.getArrivalCity());
if (quote.getDeliveryDate() != null) entity.setDeliveryDate(quote.getDeliveryDate());
if (quote.getUser() != null && quote.getUser().getUsername() != null) {
userDao.findByUsernameOrEmail(quote.getUser().getUsername()).ifPresent(entity::setUser);
}
if (quote.getCompany() != null && quote.getCompany().getId() != null) {
companyDao.findById(quote.getCompany().getId()).ifPresent(entity::setCompany);
}
QuoteEntity saved = quoteDao.save(entity);
return quoteMapper.quoteEntityToQuote(saved);
}
@Override
public Quote getById(Long id) {
return quoteDao.findById(id)
.map(quoteMapper::quoteEntityToQuote)
.orElseThrow();
}
@Override
public java.util.List<Quote> list() {
return quoteDao.findAll().stream()
.map(quoteMapper::quoteEntityToQuote)
.collect(java.util.stream.Collectors.toList());
}
@Override
public void delete(Long id) {
quoteDao.deleteById(id);
}
}

View File

@ -4,12 +4,18 @@ import com.dh7789dev.xpeditis.UserRepository;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.entity.Role;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Repository;
import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;
@Repository
public class UserJpaRepository implements UserRepository {
@ -17,11 +23,15 @@ public class UserJpaRepository implements UserRepository {
private final UserDao userDao;
private final PasswordEncoder passwordEncoder;
private final CompanyDao companyDao;
private final UserMapper userMapper;
@Autowired
public UserJpaRepository(UserDao userDao, PasswordEncoder passwordEncoder) {
public UserJpaRepository(UserDao userDao, PasswordEncoder passwordEncoder, CompanyDao companyDao, UserMapper userMapper) {
this.userDao = userDao;
this.passwordEncoder = passwordEncoder;
this.companyDao = companyDao;
this.userMapper = userMapper;
}
@Override
@ -42,4 +52,55 @@ public class UserJpaRepository implements UserRepository {
userEntity.setPassword(passwordEncoder.encode(request.getNewPassword()));
userDao.save(userEntity);
}
@Override
public UserAccount create(UserAccount user) {
UserEntity entity = userMapper.userAccountToUserEntity(user);
if (user.getPassword() != null) {
entity.setPassword(passwordEncoder.encode(user.getPassword()));
}
if (user.getRole() != null) {
entity.setRole(Role.valueOf(user.getRole()));
}
if (user.getCompany() != null && user.getCompany().getId() != null) {
companyDao.findById(user.getCompany().getId()).ifPresent(entity::setCompany);
}
entity.setEnabled(true);
UserEntity saved = userDao.save(entity);
return userMapper.userEntityToUserAccount(saved);
}
@Override
public UserAccount update(Long id, UserAccount user) {
UserEntity entity = userDao.findById(id).orElseThrow();
if (user.getFirstName() != null) entity.setFirstName(user.getFirstName());
if (user.getLastName() != null) entity.setLastName(user.getLastName());
if (user.getEmail() != null) entity.setEmail(user.getEmail());
if (user.getUsername() != null) entity.setUsername(user.getUsername());
if (user.getRole() != null) entity.setRole(Role.valueOf(user.getRole()));
if (user.getCompany() != null && user.getCompany().getId() != null) {
companyDao.findById(user.getCompany().getId()).ifPresent(entity::setCompany);
}
UserEntity saved = userDao.save(entity);
return userMapper.userEntityToUserAccount(saved);
}
@Override
public UserAccount getById(Long id) {
return userDao.findById(id)
.map(userMapper::userEntityToUserAccount)
.orElseThrow();
}
@Override
public List<UserAccount> list() {
return userDao.findAll().stream()
.map(userMapper::userEntityToUserAccount)
.collect(Collectors.toList());
}
@Override
public void delete(Long id) {
userDao.deleteById(id);
}
}

View File

@ -1,19 +1,123 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.dao.QuoteDao;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.Quote;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.entity.CompanyEntity;
import com.dh7789dev.xpeditis.entity.QuoteEntity;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.mapper.QuoteMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class QuoteJpaRepositoryTest {
private QuoteDao quoteDao;
private UserDao userDao;
private CompanyDao companyDao;
private QuoteMapper quoteMapper;
private QuoteJpaRepository repository;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
quoteDao = mock(QuoteDao.class);
userDao = mock(UserDao.class);
companyDao = mock(CompanyDao.class);
quoteMapper = new QuoteMapper() {
@Override
public QuoteEntity quoteToQuoteEntity(com.dh7789dev.xpeditis.dto.app.Quote quote) {
QuoteEntity e = new QuoteEntity();
e.setPackager(quote.getPackager());
e.setIncoterm(quote.getIncoterm());
e.setDeparturePostalCode(quote.getDeparturePostalCode());
e.setDepartureCity(quote.getDepartureCity());
return e;
}
@Override
public com.dh7789dev.xpeditis.dto.app.Quote quoteEntityToQuote(QuoteEntity quoteEntity) {
return new com.dh7789dev.xpeditis.dto.app.Quote(
quoteEntity.getId(),
quoteEntity.getPackager(),
quoteEntity.getCustomsImportExport(),
quoteEntity.getEur1(),
quoteEntity.getPackagingType(),
null,
quoteEntity.getDangerousGoods(),
quoteEntity.getTailgate(),
quoteEntity.getStraps(),
quoteEntity.getThermalCover(),
quoteEntity.getRegulatedProducts(),
quoteEntity.getAppointmentRequired(),
quoteEntity.getT1(),
quoteEntity.getCustomsStop(),
quoteEntity.getExportAssistance(),
quoteEntity.getInsurance(),
quoteEntity.getOperationType(),
quoteEntity.getIncoterm(),
quoteEntity.getDeparturePostalCode(),
quoteEntity.getDepartureCity(),
quoteEntity.getPickupDate(),
quoteEntity.getArrivalPostalCode(),
quoteEntity.getArrivalCity(),
quoteEntity.getDeliveryDate(),
null,
null,
null
);
}
};
repository = new QuoteJpaRepository(quoteDao, userDao, companyDao, quoteMapper);
}
@Test
void test(){
int test = 1 +1;
assertEquals(2,test);
void create_maps_and_saves_with_links() {
Quote dto = new Quote(
null, "ACME", null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
new UserAccount("jdoe", null, null, null, null, null, null, null, null),
new Company(1L, null, null, null, null, null),
null);
QuoteEntity entity = new QuoteEntity();
when(quoteDao.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(userDao.findByUsernameOrEmail("jdoe")).thenReturn(Optional.of(new UserEntity()));
when(companyDao.findById(1L)).thenReturn(Optional.of(new CompanyEntity()));
Quote saved = repository.create(dto);
var captor = org.mockito.ArgumentCaptor.forClass(QuoteEntity.class);
verify(quoteDao).save(captor.capture());
QuoteEntity persisted = captor.getValue();
assertThat(persisted.getUser()).isNotNull();
assertThat(persisted.getCompany()).isNotNull();
}
@Test
void update_updates_fields_and_links() {
Long id = 7L;
QuoteEntity existing = new QuoteEntity();
when(quoteDao.findById(id)).thenReturn(Optional.of(existing));
when(quoteDao.save(existing)).thenReturn(existing);
Quote dto = new Quote(
null, "PACKAGER", "IMP", true, "BOX", null, false, null, null, null, null, null,
null, null, null, null, "OP", "FOB", "75001", "Paris", null, null, null, null,
null, null, null);
Quote updated = repository.update(id, dto);
assertThat(updated).isNotNull();
assertThat(existing.getPackager()).isEqualTo("PACKAGER");
assertThat(existing.getIncoterm()).isEqualTo("FOB");
verify(quoteDao).save(existing);
}
}

View File

@ -1,19 +1,141 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.entity.Role;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.mapper.UserMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.security.Principal;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserJpaRepositoryTest {
@Mock private UserDao userDao;
@Mock private PasswordEncoder passwordEncoder;
@Mock private CompanyDao companyDao;
private UserMapper userMapper;
private UserJpaRepository repository;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
userMapper = new UserMapper() {
@Override
public UserEntity userAccountToUserEntity(com.dh7789dev.xpeditis.dto.app.UserAccount user) {
UserEntity e = new UserEntity();
e.setUsername(user.getUsername());
e.setFirstName(user.getFirstName());
e.setLastName(user.getLastName());
e.setEmail(user.getEmail());
if (user.getRole() != null) {
e.setRole(Role.valueOf(user.getRole()));
}
return e;
}
@Override
public com.dh7789dev.xpeditis.dto.app.UserAccount userEntityToUserAccount(UserEntity userEntity) {
return new com.dh7789dev.xpeditis.dto.app.UserAccount(
userEntity.getUsername(),
userEntity.getFirstName(),
userEntity.getLastName(),
userEntity.getEmail(),
null,
userEntity.getRole() != null ? userEntity.getRole().name() : null,
null,
null,
null
);
}
};
repository = new UserJpaRepository(userDao, passwordEncoder, companyDao, userMapper);
}
@Test
void test(){
int test = 1 +1;
assertEquals(2,test);
void changePassword_success() {
UserEntity principalUser = new UserEntity();
principalUser.setPassword("hashedOld");
when(passwordEncoder.matches("old", "hashedOld")).thenReturn(true);
when(passwordEncoder.encode("new")).thenReturn("hashedNew");
Principal principal = new UsernamePasswordAuthenticationToken(principalUser, null);
ChangePasswordRequest req = new ChangePasswordRequest()
.setCurrentPassword("old")
.setNewPassword("new")
.setConfirmationPassword("new");
repository.changePassword(req, principal);
assertThat(principalUser.getPassword()).isEqualTo("hashedNew");
verify(userDao).save(principalUser);
}
@Test
void changePassword_wrongCurrent_throws() {
UserEntity principalUser = new UserEntity();
principalUser.setPassword("hashedOld");
when(passwordEncoder.matches("wrong", "hashedOld")).thenReturn(false);
Principal principal = new UsernamePasswordAuthenticationToken(principalUser, null);
ChangePasswordRequest req = new ChangePasswordRequest()
.setCurrentPassword("wrong")
.setNewPassword("new")
.setConfirmationPassword("new");
assertThrows(IllegalStateException.class, () -> repository.changePassword(req, principal));
}
@Test
void create_encodesPassword_and_saves() {
UserAccount dto = new UserAccount("jdoe", "John", "Doe", "john@ex.com", "pwd", "ADMIN", new Company(1L, null, null, null, null, null), null, null);
when(userDao.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(passwordEncoder.encode("pwd")).thenReturn("hashed");
UserAccount saved = repository.create(dto);
assertThat(saved.getUsername()).isEqualTo("jdoe");
assertThat(saved.getFirstName()).isEqualTo("John");
assertThat(saved.getLastName()).isEqualTo("Doe");
assertThat(saved.getEmail()).isEqualTo("john@ex.com");
var captor = ArgumentCaptor.forClass(UserEntity.class);
verify(userDao).save(captor.capture());
UserEntity persisted = captor.getValue();
assertThat(persisted.getPassword()).isEqualTo("hashed");
assertThat(persisted.getRole()).isEqualTo(Role.ADMIN);
}
@Test
void update_updatesSelectedFields() {
Long id = 42L;
UserAccount update = new UserAccount("newuser", "Jane", null, null, null, null, null, null, null);
UserEntity existing = new UserEntity();
when(userDao.findById(id)).thenReturn(Optional.of(existing));
when(userDao.save(existing)).thenReturn(existing);
UserAccount result = repository.update(id, update);
assertThat(result).isNotNull();
assertThat(existing.getFirstName()).isEqualTo("Jane");
assertThat(existing.getUsername()).isEqualTo("newuser");
verify(userDao).save(existing);
}
}