feature login autho2

This commit is contained in:
David 2025-08-19 00:21:25 +02:00
parent e143963a73
commit e8a74d6edc
25 changed files with 284 additions and 300 deletions

View File

@ -11,8 +11,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -36,10 +34,4 @@ public class AuthenticationRestController {
return ResponseEntity.ok(service.register(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,20 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import jakarta.servlet.http.HttpServletResponse;
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;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/auth/oauth2")
public class OAuth2Controller {
@GetMapping("/authorization/{provider}")
public void redirectToOAuth2Provider(@PathVariable String provider, HttpServletResponse response) throws IOException {
response.sendRedirect("/oauth2/authorization/" + provider);
}
}

View File

@ -30,6 +30,11 @@
<artifactId>service</artifactId> <artifactId>service</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.dh7789dev</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.dh7789dev</groupId> <groupId>com.dh7789dev</groupId>
<artifactId>infrastructure</artifactId> <artifactId>infrastructure</artifactId>

View File

@ -0,0 +1,44 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class AuthenticationConfiguration {
private static final String USER_NOT_FOUND_MSG = "User %s not found";
private final UserDao userDao;
public AuthenticationConfiguration(UserDao userDao) {
this.userDao = userDao;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return identifier -> userDao.findByUsernameOrEmail(identifier)
.orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, identifier)));
}
@Bean
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder);
provider.setUserDetailsService(userDetailsService);
return provider;
}
}

View File

@ -1,49 +0,0 @@
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

@ -1,13 +1,11 @@
package com.dh7789dev.xpeditis.configuration; package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -25,14 +23,6 @@ import java.util.TimeZone;
@EnableAsync @EnableAsync
public class GlobalConfiguration { public class GlobalConfiguration {
private static final String USER_NOT_FOUND_MSG = "User %s not found";
private final UserDao userDao;
public GlobalConfiguration(UserDao userDao) {
this.userDao = userDao;
}
@Bean @Bean
public CommonsRequestLoggingFilter loggingFilter() { public CommonsRequestLoggingFilter loggingFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
@ -86,10 +76,4 @@ public class GlobalConfiguration {
return source; return source;
} }
@Bean
public UserDetailsService userDetailsService() {
return identifier -> userDao.findByUsernameOrEmail(identifier)
.orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, identifier)));
}
} }

View File

@ -1,51 +1,48 @@
package com.dh7789dev.xpeditis.configuration; package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.TokenDao; import com.dh7789dev.xpeditis.AuthenticationRepository;
import com.dh7789dev.xpeditis.dao.UserDao; import com.dh7789dev.xpeditis.CommonUtil;
import com.dh7789dev.xpeditis.entity.TokenEntity; import com.dh7789dev.xpeditis.UserRepository;
import com.dh7789dev.xpeditis.entity.UserEntity; import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.util.JwtUtil; import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
@Component // Suppression de @Component - le bean sera créé dans SecurityConfiguration
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final UserDao userDao; private final UserRepository userRepository;
private final TokenDao tokenDao; private final AuthenticationRepository authenticationRepository;
private final JwtUtil jwtUtil;
public OAuth2AuthenticationSuccessHandler(UserRepository userRepository,
AuthenticationRepository authenticationRepository) {
this.userRepository = userRepository;
this.authenticationRepository = authenticationRepository;
}
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { public void onAuthenticationSuccess(HttpServletRequest request,
String email = authentication.getName(); HttpServletResponse response,
UserEntity user = userDao.findByEmail(email) Authentication authentication) throws IOException, ServletException {
.orElseThrow(() -> new UsernameNotFoundException("User not found for email: " + email));
String accessToken = jwtUtil.generateToken(user); DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
String refreshToken = jwtUtil.generateRefreshToken(user); String email = (String) oAuth2User.getAttributes().get("email");
String password = CommonUtil.generatePassword(12);
// Save token (like classic login) UserAccount user = userRepository.findOrCreateOAuthUser(email, oAuth2User.getAttributes(), password);
TokenEntity tokenEntity = new TokenEntity() AuthenticationResponse authResponse = authenticationRepository.authenticate(
.setUser(user) new AuthenticationRequest(user.getEmail(), user.getPassword()));
.setToken(accessToken)
.setTokenType(TokenEntity.Type.BEARER)
.setExpired(false)
.setRevoked(false);
tokenDao.save(tokenEntity);
response.setContentType("application/json"); response.setContentType("application/json");
response.getWriter().write("{\"access_token\":\"" + accessToken + "\", \"refresh_token\":\"" + refreshToken + "\"}"); response.setCharacterEncoding("UTF-8");
response.getWriter().flush(); new ObjectMapper().writeValue(response.getWriter(), authResponse);
} }
} }

View File

@ -7,7 +7,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.configuration.EnableWebSecurity;
@ -15,35 +14,33 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import com.dh7789dev.xpeditis.UserRepository;
import com.dh7789dev.xpeditis.AuthenticationRepository;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
//@EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Value("${application.csrf.enabled}") @Value("${application.csrf.enabled}")
boolean csrfEnabled; boolean csrfEnabled;
private static final String ADMIN_ROLE = "ADMIN"; 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 API_V1_URI = "/api/v1/**";
private static final String[] WHITE_LIST_URL = { private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**", "/api/v1/auth/**",
"/actuator/health/**", "/actuator/health/**"
"/oauth2/**", };
"/login/oauth2/**"};
private static final String[] INTERNAL_WHITE_LIST_URL = { private static final String[] INTERNAL_WHITE_LIST_URL = {
"/v2/api-docs", "/v2/api-docs",
@ -56,28 +53,26 @@ public class SecurityConfiguration {
"/swagger-ui/**", "/swagger-ui/**",
"/webjars/**", "/webjars/**",
"/swagger-ui.html", "/swagger-ui.html",
"/addevent"}; "/addevent"
};
private static final WebExpressionAuthorizationManager INTERNAL_ACCESS = private static final WebExpressionAuthorizationManager INTERNAL_ACCESS =
new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1') or hasIpAddress('0:0:0:0:0:0:0:1')"); new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1') or hasIpAddress('0:0:0:0:0:0:0:1')");
private final UserDetailsService userDetailsService; private final UserDetailsService userDetailsService;
private final LogoutHandler logoutHandler; private final LogoutHandler logoutHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final ClientRegistrationRepository clientRegistrationRepository; private final ClientRegistrationRepository clientRegistrationRepository;
private final AuthenticationProvider authenticationProvider;
@Autowired @Autowired
public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler, public SecurityConfiguration(UserDetailsService userDetailsService,
CustomOAuth2UserService customOAuth2UserService, LogoutHandler logoutHandler,
OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, @Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository) { AuthenticationProvider authenticationProvider) {
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
this.logoutHandler = logoutHandler; this.logoutHandler = logoutHandler;
this.customOAuth2UserService = customOAuth2UserService;
this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler;
this.clientRegistrationRepository = clientRegistrationRepository; this.clientRegistrationRepository = clientRegistrationRepository;
this.authenticationProvider = authenticationProvider;
} }
@Bean @Bean
@ -86,20 +81,16 @@ public class SecurityConfiguration {
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler(
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); UserRepository userRepository,
AuthenticationRepository authenticationRepository) {
return new OAuth2AuthenticationSuccessHandler(userRepository, authenticationRepository);
} }
@Bean @Bean
public AuthenticationProvider authenticationProvider() { public SecurityFilterChain securityFilterChain(HttpSecurity http,
final DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); JwtAuthenticationFilter jwtAuthFilter,
provider.setPasswordEncoder(passwordEncoder()); OAuth2AuthenticationSuccessHandler successHandler) throws Exception {
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter) throws Exception {
if (csrfEnabled) { if (csrfEnabled) {
http.csrf(csrf -> csrf.ignoringRequestMatchers(antMatcher("/h2-console/**"))); http.csrf(csrf -> csrf.ignoringRequestMatchers(antMatcher("/h2-console/**")));
@ -107,27 +98,30 @@ public class SecurityConfiguration {
// csrf is disabled for dev and test // csrf is disabled for dev and test
http.csrf(AbstractHttpConfigurer::disable); http.csrf(AbstractHttpConfigurer::disable);
} }
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
http.authorizeHttpRequests(auth -> http.authorizeHttpRequests(auth ->
auth.requestMatchers(WHITE_LIST_URL).permitAll() auth.requestMatchers("/oauth2/authorization/**").permitAll()
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll() .requestMatchers("/login/oauth2/**").permitAll()
.requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll() .requestMatchers(WHITE_LIST_URL).permitAll()
.requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE) .requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
.requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE) .requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll()
.requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE) .requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS) .requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS) .requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS) .requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
.anyRequest().authenticated() .requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)
.anyRequest().authenticated()
) )
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider()) .authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
if (clientRegistrationRepository != null) { if (clientRegistrationRepository != null) {
http.oauth2Login(oauth -> oauth http.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) .successHandler(successHandler)
.successHandler(oAuth2AuthenticationSuccessHandler)
); );
} }

View File

@ -1,26 +1,20 @@
spring:
security: security:
oauth2: oauth2:
client: client:
registration: registration:
google: google:
client-id: ${GOOGLE_CLIENT_ID:dummy} client-id: 1018440977117-me57ug4lqjgvcr1mg8fq17vpc18mvnsl.apps.googleusercontent.com
client-secret: ${GOOGLE_CLIENT_SECRET:dummy} client-secret: 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
scope: openid,profile,email scope:
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - profile
linkedin: - email
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: provider:
linkedin: google:
authorization-uri: https://www.linkedin.com/oauth/v2/authorization authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.linkedin.com/oauth/v2/accessToken token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://api.linkedin.com/v2/userinfo user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub user-name-attribute: sub
---
spring:
h2: h2:
console: console:
enabled: 'false' enabled: 'false'
@ -80,3 +74,6 @@ application:
expiration: 86400000 # a day expiration: 86400000 # a day
refresh-token: refresh-token:
expiration: 604800000 # 7 days expiration: 604800000 # 7 days
server:
port: 8083

View File

@ -17,33 +17,6 @@ import static org.mockito.Mockito.*;
class OAuth2AuthenticationSuccessHandlerTest { 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,8 +1,47 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import java.security.SecureRandom;
public class CommonUtil { public class CommonUtil {
public void sayHello() { private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
System.out.println("Hello!"); private static final String LOWER = "abcdefghijklmnopqrstuvwxyz";
private static final String DIGITS = "0123456789";
private static final String SPECIAL = "!@#$%^&*()-_=+[]{}|;:,.<>?";
private static final String ALL = UPPER + LOWER + DIGITS + SPECIAL;
private static final SecureRandom random = new SecureRandom();
public static String generatePassword(int length) {
if (length < 8) {
throw new IllegalArgumentException("Password length should be at least 8 characters");
}
StringBuilder password = new StringBuilder(length);
// Garantir au moins un caractère de chaque catégorie
password.append(UPPER.charAt(random.nextInt(UPPER.length())));
password.append(LOWER.charAt(random.nextInt(LOWER.length())));
password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
password.append(SPECIAL.charAt(random.nextInt(SPECIAL.length())));
// Remplir le reste avec des caractères aléatoires
for (int i = 4; i < length; i++) {
password.append(ALL.charAt(random.nextInt(ALL.length())));
}
// Mélanger les caractères pour éviter un ordre prévisible
return shuffleString(password.toString());
}
private static String shuffleString(String input) {
char[] a = input.toCharArray();
for (int i = a.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
char temp = a[i];
a[i] = a[j];
a[j] = temp;
}
return new String(a);
} }
} }

View File

@ -5,6 +5,7 @@ import com.dh7789dev.xpeditis.dto.app.UserAccount;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
import java.util.Map;
public interface UserRepository { public interface UserRepository {
@ -19,4 +20,7 @@ public interface UserRepository {
List<UserAccount> list(); List<UserAccount> list();
void delete(Long id); void delete(Long id);
UserAccount findOrCreateOAuthUser(String email, Map<String, Object> attributes,String password);
} }

View File

@ -5,13 +5,10 @@ import com.dh7789dev.xpeditis.dto.app.Address;
import com.dh7789dev.xpeditis.entity.AddressEntity; import com.dh7789dev.xpeditis.entity.AddressEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { CompanyMapper.class }) @Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { CompanyMapper.class })
public interface AddressMapper { public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
AddressEntity addressToAddressEntity(Address address); AddressEntity addressToAddressEntity(Address address);
Address addressEntityToAddress(AddressEntity addressEntity); Address addressEntityToAddress(AddressEntity addressEntity);

View File

@ -1,20 +1,14 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Company; import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.entity.CompanyEntity; import com.dh7789dev.xpeditis.entity.CompanyEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) @Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface CompanyMapper { public interface CompanyMapper {
CompanyMapper INSTANCE = Mappers.getMapper(CompanyMapper.class);
@Mapping(target = "createdDate", ignore = true) @Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true) @Mapping(target = "modifiedDate", ignore = true)
@Mapping(target = "createdBy", ignore = true) @Mapping(target = "createdBy", ignore = true)

View File

@ -4,11 +4,10 @@ package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Dimension; import com.dh7789dev.xpeditis.dto.app.Dimension;
import com.dh7789dev.xpeditis.entity.DimensionEntity; import com.dh7789dev.xpeditis.entity.DimensionEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public interface DimensionMapper { public interface DimensionMapper {
DimensionMapper INSTANCE = Mappers.getMapper(DimensionMapper.class);
DimensionEntity dimensionToDimensionEntity(Dimension dimension); DimensionEntity dimensionToDimensionEntity(Dimension dimension);

View File

@ -3,11 +3,10 @@ package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Document; import com.dh7789dev.xpeditis.dto.app.Document;
import com.dh7789dev.xpeditis.entity.DocumentEntity; import com.dh7789dev.xpeditis.entity.DocumentEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public interface DocumentMapper { public interface DocumentMapper {
DocumentMapper INSTANCE = Mappers.getMapper(DocumentMapper.class);
DocumentEntity documentToDocumentEntity(Document document); DocumentEntity documentToDocumentEntity(Document document);
Document documentEntityToDocument(DocumentEntity documentEntity); Document documentEntityToDocument(DocumentEntity documentEntity);

View File

@ -5,11 +5,10 @@ import com.dh7789dev.xpeditis.entity.ExportFolderEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { DocumentMapper.class, CompanyMapper.class }) @Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { DocumentMapper.class, CompanyMapper.class })
public interface ExportFolderMapper { public interface ExportFolderMapper {
ExportFolderMapper INSTANCE = Mappers.getMapper(ExportFolderMapper.class);
@Mapping(target = "createdDate", ignore = true) @Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true) @Mapping(target = "modifiedDate", ignore = true)

View File

@ -1,16 +1,12 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.License; import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.entity.LicenseEntity; import com.dh7789dev.xpeditis.entity.LicenseEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) @Mapper(componentModel = "spring")
public interface LicenseMapper { public interface LicenseMapper {
LicenseMapper INSTANCE = Mappers.getMapper(LicenseMapper.class);
@Mapping(target = "createdDate", ignore = true) @Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true) @Mapping(target = "modifiedDate", ignore = true)

View File

@ -4,12 +4,10 @@ package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Notification; import com.dh7789dev.xpeditis.dto.app.Notification;
import com.dh7789dev.xpeditis.entity.NotificationEntity; import com.dh7789dev.xpeditis.entity.NotificationEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring" , uses = { ExportFolderMapper.class }) @Mapper(componentModel = "spring" , uses = { ExportFolderMapper.class })
public interface NotificationMapper { public interface NotificationMapper {
NotificationMapper INSTANCE = Mappers.getMapper(NotificationMapper.class);
NotificationEntity notificationToNotificationEntity(Notification notification); NotificationEntity notificationToNotificationEntity(Notification notification);

View File

@ -4,11 +4,10 @@ import com.dh7789dev.xpeditis.dto.app.QuoteDetail;
import com.dh7789dev.xpeditis.entity.QuoteDetailEntity; import com.dh7789dev.xpeditis.entity.QuoteDetailEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring", uses = { DimensionMapper.class }) @Mapper(componentModel = "spring", uses = { DimensionMapper.class })
public interface QuoteDetailMapper { public interface QuoteDetailMapper {
QuoteDetailMapper INSTANCE = Mappers.getMapper(QuoteDetailMapper.class);
@Mapping(source = "quoteId", target = "quote.id") @Mapping(source = "quoteId", target = "quote.id")
QuoteDetailEntity quoteDetailsToQuoteDetailsEntity(QuoteDetail quoteDetail); QuoteDetailEntity quoteDetailsToQuoteDetailsEntity(QuoteDetail quoteDetail);

View File

@ -5,11 +5,10 @@ import com.dh7789dev.xpeditis.entity.QuoteEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { QuoteDetailMapper.class }) @Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { QuoteDetailMapper.class })
public interface QuoteMapper { public interface QuoteMapper {
QuoteMapper INSTANCE = Mappers.getMapper(QuoteMapper.class);
@Mapping(target = "createdDate", ignore = true) @Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true) @Mapping(target = "modifiedDate", ignore = true)

View File

@ -1,16 +1,12 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.ShipmentTracking; import com.dh7789dev.xpeditis.dto.app.ShipmentTracking;
import com.dh7789dev.xpeditis.entity.ShipmentTrackingEntity; import com.dh7789dev.xpeditis.entity.ShipmentTrackingEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring", uses = { ExportFolderMapper.class }) @Mapper(componentModel = "spring", uses = { ExportFolderMapper.class })
public interface ShipmentTrackingMapper { public interface ShipmentTrackingMapper {
ShipmentTrackingMapper INSTANCE = Mappers.getMapper(ShipmentTrackingMapper.class);
ShipmentTrackingEntity shipmentTrackingToShipmentTrackingEntity(ShipmentTracking shipmentTracking); ShipmentTrackingEntity shipmentTrackingToShipmentTrackingEntity(ShipmentTracking shipmentTracking);
ShipmentTracking shipmentTrackingEntityToShipmentTracking(ShipmentTrackingEntity shipmentTrackingEntity); ShipmentTracking shipmentTrackingEntityToShipmentTracking(ShipmentTrackingEntity shipmentTrackingEntity);

View File

@ -5,14 +5,10 @@ import com.dh7789dev.xpeditis.entity.UserEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) @Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserMapper { public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(target = "createdDate", ignore = true) @Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true) @Mapping(target = "modifiedDate", ignore = true)
@Mapping(target = "createdBy", ignore = true) @Mapping(target = "createdBy", ignore = true)

View File

@ -1,16 +1,12 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.VesselSchedule; import com.dh7789dev.xpeditis.dto.app.VesselSchedule;
import com.dh7789dev.xpeditis.entity.VesselScheduleEntity; import com.dh7789dev.xpeditis.entity.VesselScheduleEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring", uses = { ExportFolderMapper.class }) @Mapper(componentModel = "spring", uses = { ExportFolderMapper.class })
public interface VesselScheduleMapper { public interface VesselScheduleMapper {
VesselScheduleMapper INSTANCE = Mappers.getMapper(VesselScheduleMapper.class);
VesselScheduleEntity vesselScheduleToVesselScheduleEntity(VesselSchedule vesselSchedule); VesselScheduleEntity vesselScheduleToVesselScheduleEntity(VesselSchedule vesselSchedule);
VesselSchedule vesselScheduleEntityToVesselSchedule(VesselScheduleEntity vesselScheduleEntity); VesselSchedule vesselScheduleEntityToVesselSchedule(VesselScheduleEntity vesselScheduleEntity);

View File

@ -15,6 +15,7 @@ import org.springframework.stereotype.Repository;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Repository @Repository
@ -103,4 +104,19 @@ public class UserJpaRepository implements UserRepository {
public void delete(Long id) { public void delete(Long id) {
userDao.deleteById(id); userDao.deleteById(id);
} }
@Override
public UserAccount findOrCreateOAuthUser(String email, Map<String, Object> attributes,String password) {
return userMapper.userEntityToUserAccount(userDao.findByEmail(email).orElseGet(() -> {
UserEntity newUser = new UserEntity();
newUser.setEmail(email);
newUser.setUsername(email);
newUser.setFirstName((String) attributes.getOrDefault("name", "Unknown"));
newUser.setLastName((String) attributes.getOrDefault("name", "Unknown"));
newUser.setPassword(passwordEncoder.encode(password));
newUser.setRole(Role.ADMIN);
newUser.setEnabled(true);
return userDao.save(newUser);
}));
}
} }