diff --git a/backend/src/main/java/de/szut/casino/CasinoApplication.java b/backend/src/main/java/de/szut/casino/CasinoApplication.java index 9a5db34..9e1f3b2 100644 --- a/backend/src/main/java/de/szut/casino/CasinoApplication.java +++ b/backend/src/main/java/de/szut/casino/CasinoApplication.java @@ -8,9 +8,6 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.mail.MailException; -import org.springframework.mail.MailSender; -import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.web.client.RestTemplate; diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java index 9bf591f..f791d19 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java @@ -112,7 +112,7 @@ public class BlackJackService { dealCardToPlayer(game); dealCardToSplitHand(game); - return blackJackGameRepository.save(game); + return processGameBasedOnState(game); } private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) { diff --git a/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java b/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java index 7af90b9..75cac52 100644 --- a/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java +++ b/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java @@ -1,7 +1,6 @@ package de.szut.casino.config; - import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; diff --git a/backend/src/main/java/de/szut/casino/config/WebConfig.java b/backend/src/main/java/de/szut/casino/config/WebConfig.java index bb11293..ecc21be 100644 --- a/backend/src/main/java/de/szut/casino/config/WebConfig.java +++ b/backend/src/main/java/de/szut/casino/config/WebConfig.java @@ -18,12 +18,12 @@ public class WebConfig { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins(frontendHost) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .exposedHeaders("*") - .allowCredentials(true) - .maxAge(3600); + .allowedOrigins(frontendHost) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("*") + .allowCredentials(true) + .maxAge(3600); } }; } diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java index 522e47d..e64e428 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -53,8 +53,8 @@ public class DepositController { .build()) .setQuantity(1L) .build()) - .setSuccessUrl(frontendHost+"/home?success=true") - .setCancelUrl(frontendHost+"/home?success=false") + .setSuccessUrl(frontendHost + "/home?success=true") + .setCancelUrl(frontendHost + "/home?success=false") .setMode(SessionCreateParams.Mode.PAYMENT) .build(); diff --git a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java index ea08367..af97d4e 100644 --- a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java +++ b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java @@ -1,7 +1,5 @@ package de.szut.casino.exceptionHandling.exceptions; -import de.szut.casino.security.service.EmailService; - public class EmailNotVerifiedException extends Exception { public EmailNotVerifiedException() { super("Email not verified"); diff --git a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/OAuth2AuthenticationProcessingException.java b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/OAuth2AuthenticationProcessingException.java new file mode 100644 index 0000000..b4f421e --- /dev/null +++ b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/OAuth2AuthenticationProcessingException.java @@ -0,0 +1,9 @@ +package de.szut.casino.exceptionHandling.exceptions; + +import org.springframework.security.core.AuthenticationException; + +public class OAuth2AuthenticationProcessingException extends AuthenticationException { + public OAuth2AuthenticationProcessingException(String msg) { + super(msg); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/AuthController.java b/backend/src/main/java/de/szut/casino/security/AuthController.java index d22de30..561fc3c 100644 --- a/backend/src/main/java/de/szut/casino/security/AuthController.java +++ b/backend/src/main/java/de/szut/casino/security/AuthController.java @@ -1,6 +1,5 @@ package de.szut.casino.security; -import de.szut.casino.exceptionHandling.ErrorDetails; import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException; import de.szut.casino.security.dto.AuthResponseDto; import de.szut.casino.security.dto.LoginRequestDto; @@ -15,12 +14,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.IOException; -import java.util.Date; @RestController @RequestMapping("/auth") public class AuthController { + @Autowired private AuthService authService; @@ -38,11 +37,11 @@ public class AuthController { @PostMapping("/verify") public ResponseEntity verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException { - if (authService.verifyEmail(token)) { - return ResponseEntity.badRequest().build(); - } + if (authService.verifyEmail(token)) { + return ResponseEntity.badRequest().build(); + } - return ResponseEntity.ok().build(); + return ResponseEntity.ok().build(); } @PostMapping("/recover-password") diff --git a/backend/src/main/java/de/szut/casino/security/CorsFilter.java b/backend/src/main/java/de/szut/casino/security/CorsFilter.java index 032088f..b1c1b3b 100644 --- a/backend/src/main/java/de/szut/casino/security/CorsFilter.java +++ b/backend/src/main/java/de/szut/casino/security/CorsFilter.java @@ -19,23 +19,22 @@ public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - + HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest request = (HttpServletRequest) req; - - // Allow requests from the frontend + response.setHeader("Access-Control-Allow-Origin", frontendHost); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "*"); response.setHeader("Access-Control-Expose-Headers", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Max-Age", "3600"); - + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } - + chain.doFilter(req, res); } } diff --git a/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java b/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java index 9f5304e..1997ac7 100644 --- a/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java +++ b/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java @@ -7,7 +7,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; public class CustomJwtAuthenticationConverter implements Converter { - + @Override public AbstractAuthenticationToken convert(Jwt source) { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); diff --git a/backend/src/main/java/de/szut/casino/security/GitHubController.java b/backend/src/main/java/de/szut/casino/security/GitHubController.java new file mode 100644 index 0000000..b45fb62 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/GitHubController.java @@ -0,0 +1,49 @@ +package de.szut.casino.security; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.dto.GithubCallbackDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +@RestController +@RequestMapping("/oauth2/github") +public class GitHubController { + private static final Logger logger = LoggerFactory.getLogger(GitHubController.class); + + @Value("${spring.security.oauth2.client.registration.github.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.provider.github.authorization-uri}") + private String authorizationUri; + + @Value("${spring.security.oauth2.client.registration.github.redirect-uri}") + private String redirectUri; + + @Autowired + private GitHubService githubService; + + @GetMapping("/authorize") + public RedirectView authorizeGithub() { + logger.info("Redirecting to GitHub for authorization"); + + String authUrl = authorizationUri + + "?client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&scope=user:email,read:user"; + + return new RedirectView(authUrl); + } + + + @PostMapping("/callback") + public ResponseEntity githubCallback(@RequestBody GithubCallbackDto githubCallbackDto) { + String code = githubCallbackDto.getCode(); + AuthResponseDto response = githubService.processGithubCode(code); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/GitHubService.java b/backend/src/main/java/de/szut/casino/security/GitHubService.java new file mode 100644 index 0000000..1051498 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/GitHubService.java @@ -0,0 +1,165 @@ +package de.szut.casino.security; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.jwt.JwtUtils; +import de.szut.casino.user.AuthProvider; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.*; + +@Service +public class GitHubService { + @Value("${spring.security.oauth2.client.registration.github.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.github.client-secret}") + private String clientSecret; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private PasswordEncoder oauth2PasswordEncoder; + + public AuthResponseDto processGithubCode(String code) { + try { + RestTemplate restTemplate = new RestTemplate(); + + Map requestBody = new HashMap<>(); + requestBody.put("client_id", clientId); + requestBody.put("client_secret", clientSecret); + requestBody.put("code", code); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Accept", "application/json"); + + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response = restTemplate.exchange( + "https://github.com/login/oauth/access_token", + HttpMethod.POST, + requestEntity, + Map.class + ); + + Map responseBody = response.getBody(); + + if (responseBody.containsKey("error")) { + String error = (String) responseBody.get("error"); + String errorDescription = (String) responseBody.get("error_description"); + + throw new RuntimeException("GitHub OAuth error: " + errorDescription); + } + + String accessToken = (String) responseBody.get("access_token"); + if (accessToken == null || accessToken.isEmpty()) { + + throw new RuntimeException("Failed to receive access token from GitHub"); + } + + HttpHeaders userInfoHeaders = new HttpHeaders(); + userInfoHeaders.set("Authorization", "Bearer " + accessToken); + + HttpEntity userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders); + + ResponseEntity userResponse = restTemplate.exchange( + "https://api.github.com/user", + HttpMethod.GET, + userInfoRequestEntity, + Map.class + ); + + Map userAttributes = userResponse.getBody(); + + HttpHeaders emailsHeaders = new HttpHeaders(); + emailsHeaders.set("Authorization", "Bearer " + accessToken); + + HttpEntity emailsRequestEntity = new HttpEntity<>(null, emailsHeaders); + + ResponseEntity emailsResponse = restTemplate.exchange( + "https://api.github.com/user/emails", + HttpMethod.GET, + emailsRequestEntity, + List.class + ); + + List> emails = emailsResponse.getBody(); + String email = null; + + for (Map emailInfo : emails) { + Boolean primary = (Boolean) emailInfo.get("primary"); + if (primary != null && primary) { + email = (String) emailInfo.get("email"); + break; + } + } + + if (email == null && !emails.isEmpty()) { + email = (String) emails.get(0).get("email"); + } + + String githubId = userAttributes.get("id").toString(); + String username = (String) userAttributes.get("login"); + + Optional userOptional = userRepository.findByProviderId(githubId); + UserEntity user; + + if (userOptional.isPresent()) { + user = userOptional.get(); + } else { + userOptional = userRepository.findByEmail(email); + + if (userOptional.isPresent()) { + user = userOptional.get(); + user.setProvider(AuthProvider.GITHUB); + user.setProviderId(githubId); + } else { + user = new UserEntity(); + user.setEmail(email); + user.setUsername(username); + user.setProvider(AuthProvider.GITHUB); + user.setProviderId(githubId); + user.setEmailVerified(true); + + user.setBalance(new BigDecimal("1000.00")); + } + } + + String randomPassword = UUID.randomUUID().toString(); + user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); + + userRepository.save(user); + + Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)); + + String token = jwtUtils.generateToken(authentication); + + return new AuthResponseDto(token); + + } catch (Exception e) { + throw new RuntimeException("Failed to process GitHub authentication", e); + } + } +} diff --git a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java index 155de8d..740739e 100644 --- a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java +++ b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java @@ -38,13 +38,14 @@ public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - + authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); - + return authProvider; } @@ -61,16 +62,16 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> { - auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error").permitAll() - .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() - .anyRequest().authenticated(); + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> { + auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error", "/oauth2/**").permitAll() + .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated(); }) - .authenticationProvider(authenticationProvider()) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java b/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java index 0dd03dd..4dfe79a 100644 --- a/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java +++ b/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java @@ -12,7 +12,7 @@ import lombok.Setter; public class AuthResponseDto { private String token; private String tokenType = "Bearer"; - + public AuthResponseDto(String token) { this.token = token; } diff --git a/backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java b/backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java new file mode 100644 index 0000000..07619aa --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java @@ -0,0 +1,8 @@ +package de.szut.casino.security.dto; + +import lombok.Data; + +@Data +public class GithubCallbackDto { + private String code; +} diff --git a/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java b/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java index ace3508..767a6d3 100644 --- a/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java +++ b/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java @@ -13,7 +13,7 @@ import lombok.Setter; public class LoginRequestDto { @NotBlank(message = "Username or email is required") private String usernameOrEmail; - + @NotBlank(message = "Password is required") private String password; } diff --git a/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java b/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java index 502ba0a..38da0b3 100644 --- a/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java +++ b/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java @@ -35,11 +35,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - + if (jwtUtils.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); - + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } diff --git a/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java b/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java index 3c5ef86..1a7d08d 100644 --- a/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java +++ b/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java @@ -1,12 +1,16 @@ package de.szut.casino.security.jwt; +import de.szut.casino.security.oauth2.UserPrincipal; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Component; import java.security.Key; @@ -17,6 +21,7 @@ import java.util.function.Function; @Component public class JwtUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); @Value("${jwt.secret}") private String jwtSecret; @@ -29,8 +34,26 @@ public class JwtUtils { } public String generateToken(Authentication authentication) { - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - return generateToken(userDetails.getUsername()); + String subject = null; + Map claims = new HashMap<>(); + + if (authentication.getPrincipal() instanceof UserPrincipal) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + subject = userPrincipal.getEmail(); + claims.put("id", userPrincipal.getId()); + claims.put("username", userPrincipal.getDisplayUsername()); + logger.info("Generating token for UserPrincipal: {}", subject); + } else if (authentication.getPrincipal() instanceof OAuth2User) { + OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal(); + subject = (String) oauth2User.getAttributes().get("email"); + logger.info("Generating token for OAuth2User: {}", subject); + } else { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + subject = userDetails.getUsername(); + logger.info("Generating token for UserDetails: {}", subject); + } + + return createToken(claims, subject); } public String generateToken(String username) { @@ -40,6 +63,9 @@ public class JwtUtils { private String createToken(Map claims, String subject) { Date now = new Date(); + logger.info("now: {}", now); + logger.info("jwtExpirationMs: {}", jwtExpirationMs); + logger.info("expiryDate: {}", new Date(now.getTime() + jwtExpirationMs)); Date expiryDate = new Date(now.getTime() + jwtExpirationMs); return Jwts.builder() diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/CustomOAuth2UserService.java b/backend/src/main/java/de/szut/casino/security/oauth2/CustomOAuth2UserService.java new file mode 100644 index 0000000..eb09b27 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,105 @@ +package de.szut.casino.security.oauth2; + +import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException; +import de.szut.casino.user.AuthProvider; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +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.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.UUID; + +@Service +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder oauth2PasswordEncoder; + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); + + try { + return processOAuth2User(oAuth2UserRequest, oAuth2User); + } catch (AuthenticationException ex) { + throw ex; + } catch (Exception ex) { + throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause()); + } + } + + private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { + String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId(); + OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes()); + + String email = oAuth2UserInfo.getEmail(); + if (StringUtils.isEmpty(email)) { + email = oAuth2UserInfo.getName() + "@github.user"; + } + + Optional userOptional = userRepository.findByEmail(email); + UserEntity user; + + if (userOptional.isPresent()) { + user = userOptional.get(); + + if (!user.getProvider().equals(AuthProvider.valueOf(registrationId.toUpperCase()))) { + throw new OAuth2AuthenticationProcessingException("You're signed up with " + + user.getProvider() + ". Please use your " + user.getProvider() + + " account to login."); + } + + user = updateExistingUser(user, oAuth2UserInfo); + } else { + user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo, email); + } + + return UserPrincipal.create(user, oAuth2User.getAttributes()); + } + + private UserEntity registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo, String email) { + UserEntity user = new UserEntity(); + + String username = oAuth2UserInfo.getName(); + if (StringUtils.isEmpty(username)) { + username = "github_" + oAuth2UserInfo.getId(); + } + + if (userRepository.findByUsername(username).isPresent()) { + username = username + "_" + UUID.randomUUID().toString().substring(0, 8); + } + + user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId().toUpperCase())); + user.setProviderId(oAuth2UserInfo.getId()); + user.setUsername(username); + user.setEmail(email); + user.setEmailVerified(true); + + String randomPassword = UUID.randomUUID().toString(); + user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); + + user.setBalance(new BigDecimal("100.00")); // Starting balance + + return userRepository.save(user); + } + + private UserEntity updateExistingUser(UserEntity existingUser, OAuth2UserInfo oAuth2UserInfo) { + if (!StringUtils.isEmpty(oAuth2UserInfo.getName())) { + existingUser.setUsername(oAuth2UserInfo.getName()); + } + return userRepository.save(existingUser); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java b/backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java new file mode 100644 index 0000000..f98390e --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java @@ -0,0 +1,25 @@ +package de.szut.casino.security.oauth2; + +import java.util.Map; + +public class GitHubOAuth2UserInfo extends OAuth2UserInfo { + + public GitHubOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return ((Integer) attributes.get("id")).toString(); + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2AuthenticationSuccessHandler.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000..778fa88 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,55 @@ +package de.szut.casino.security.oauth2; + +import de.szut.casino.security.jwt.JwtUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationSuccessHandler.class); + + @Value("${app.oauth2.authorizedRedirectUris}") + private String redirectUri; + + @Autowired + private JwtUtils jwtUtils; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + String targetUrl = determineTargetUrl(authentication); + + logger.info("OAuth2 Authentication successful, redirecting to: {}", targetUrl); + + if (response.isCommitted()) { + logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); + return; + } + + clearAuthenticationAttributes(request); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + private String determineTargetUrl(Authentication authentication) { + String token = jwtUtils.generateToken(authentication); + + if (authentication.getPrincipal() instanceof UserPrincipal) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + logger.info("User authenticated: ID={}, Email={}", userPrincipal.getId(), userPrincipal.getEmail()); + } + + return UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("token", token) + .build().toUriString(); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2Config.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2Config.java new file mode 100644 index 0000000..32104ed --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2Config.java @@ -0,0 +1,15 @@ +package de.szut.casino.security.oauth2; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class OAuth2Config { + + @Bean + public PasswordEncoder oauth2PasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfo.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfo.java new file mode 100644 index 0000000..14e2bcc --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfo.java @@ -0,0 +1,20 @@ +package de.szut.casino.security.oauth2; + +import lombok.Getter; + +import java.util.Map; + +@Getter +public abstract class OAuth2UserInfo { + protected Map attributes; + + public OAuth2UserInfo(Map attributes) { + this.attributes = attributes; + } + + public abstract String getId(); + + public abstract String getName(); + + public abstract String getEmail(); +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java new file mode 100644 index 0000000..b7d4365 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java @@ -0,0 +1,17 @@ +package de.szut.casino.security.oauth2; + +import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException; +import de.szut.casino.user.AuthProvider; + +import java.util.Map; + +public class OAuth2UserInfoFactory { + + public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { + if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) { + return new GitHubOAuth2UserInfo(attributes); + } else { + throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); + } + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/UserPrincipal.java b/backend/src/main/java/de/szut/casino/security/oauth2/UserPrincipal.java new file mode 100644 index 0000000..8ec25e0 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/UserPrincipal.java @@ -0,0 +1,102 @@ +package de.szut.casino.security.oauth2; + +import de.szut.casino.user.UserEntity; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class UserPrincipal implements OAuth2User, UserDetails { + @Getter + private Long id; + @Getter + private String email; + private String username; + private String password; + private Collection authorities; + @Setter + private Map attributes; + + public UserPrincipal(Long id, String email, String username, String password, Collection authorities) { + this.id = id; + this.email = email; + this.username = username; + this.password = password; + this.authorities = authorities; + } + + public static UserPrincipal create(UserEntity user) { + List authorities = Collections. + singletonList(new SimpleGrantedAuthority("ROLE_USER")); + + return new UserPrincipal( + user.getId(), + user.getEmail(), + user.getUsername(), + user.getPassword(), + authorities + ); + } + + public static UserPrincipal create(UserEntity user, Map attributes) { + UserPrincipal userPrincipal = UserPrincipal.create(user); + userPrincipal.setAttributes(attributes); + return userPrincipal; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + public String getDisplayUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return String.valueOf(id); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/service/AuthService.java b/backend/src/main/java/de/szut/casino/security/service/AuthService.java index 959a55a..ffe06ae 100644 --- a/backend/src/main/java/de/szut/casino/security/service/AuthService.java +++ b/backend/src/main/java/de/szut/casino/security/service/AuthService.java @@ -19,7 +19,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import javax.swing.text.html.Option; import java.io.IOException; import java.util.Optional; diff --git a/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java b/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java index b473a35..8d4018b 100644 --- a/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java +++ b/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java @@ -20,17 +20,17 @@ public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { Optional user = userRepository.findByUsername(usernameOrEmail); - + if (user.isEmpty()) { user = userRepository.findByEmail(usernameOrEmail); } - - UserEntity userEntity = user.orElseThrow(() -> + + UserEntity userEntity = user.orElseThrow(() -> new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail)); - + return new org.springframework.security.core.userdetails.User( - userEntity.getUsername(), - userEntity.getPassword(), + userEntity.getUsername(), + userEntity.getPassword(), new ArrayList<>()); } } diff --git a/backend/src/main/java/de/szut/casino/user/AuthProvider.java b/backend/src/main/java/de/szut/casino/user/AuthProvider.java new file mode 100644 index 0000000..2216da7 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/AuthProvider.java @@ -0,0 +1,6 @@ +package de.szut.casino.user; + +public enum AuthProvider { + LOCAL, + GITHUB +} diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 2190867..c263d9c 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -1,9 +1,6 @@ package de.szut.casino.user; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -18,15 +15,15 @@ public class UserEntity { @Id @GeneratedValue private Long id; - + @Column(unique = true) private String email; - + @Column(unique = true) private String username; - + private String password; - + @Column(precision = 19, scale = 2) private BigDecimal balance; @@ -36,12 +33,27 @@ public class UserEntity { private String passwordResetToken; + @Enumerated(EnumType.STRING) + private AuthProvider provider = AuthProvider.LOCAL; + + private String providerId; + public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) { this.email = email; this.username = username; this.password = password; this.balance = balance; this.verificationToken = verificationToken; + this.provider = AuthProvider.LOCAL; + } + + public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) { + this.email = email; + this.username = username; + this.provider = provider; + this.providerId = providerId; + this.balance = balance; + this.emailVerified = true; // OAuth providers verify emails } public void addBalance(BigDecimal amountToAdd) { diff --git a/backend/src/main/java/de/szut/casino/user/UserMappingService.java b/backend/src/main/java/de/szut/casino/user/UserMappingService.java index e244646..c93c132 100644 --- a/backend/src/main/java/de/szut/casino/user/UserMappingService.java +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Service; @Service public class UserMappingService { - + public GetUserDto mapToGetUserDto(UserEntity user) { return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance()); } diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java index 29790f5..db28f49 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -9,11 +9,13 @@ import java.util.Optional; @Service public interface UserRepository extends JpaRepository { Optional findByUsername(String username); - + Optional findByEmail(String email); - + + Optional findByProviderId(String providerId); + boolean existsByUsername(String username); - + boolean existsByEmail(String email); @Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1") diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index baa2eab..03ad02d 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -27,15 +27,15 @@ public class UserService { if (userRepository.existsByEmail(createUserDto.getEmail())) { throw new EntityExistsException("Email is already in use"); } - + UserEntity user = new UserEntity( - createUserDto.getEmail(), - createUserDto.getUsername(), - passwordEncoder.encode(createUserDto.getPassword()), - BigDecimal.valueOf(100), - RandomStringUtils.randomAlphanumeric(64) + createUserDto.getEmail(), + createUserDto.getUsername(), + passwordEncoder.encode(createUserDto.getPassword()), + BigDecimal.valueOf(100), + RandomStringUtils.randomAlphanumeric(64) ); - + return userRepository.save(user); } @@ -50,7 +50,7 @@ public class UserService { } public void saveUser(UserEntity user) { - userRepository.save(user); + userRepository.save(user); } public boolean isVerified(String usernameOrEmail) { diff --git a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java index f9969cc..1be6ae3 100644 --- a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java +++ b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java @@ -16,11 +16,11 @@ public class CreateUserDto { @NotBlank(message = "Email is required") @Email(message = "Email should be valid") private String email; - + @NotBlank(message = "Username is required") @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") private String username; - + @NotBlank(message = "Password is required") @Size(min = 6, message = "Password must be at least 6 characters") private String password; diff --git a/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java b/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java index 10061fa..8aa74a6 100644 --- a/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java +++ b/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java @@ -19,7 +19,7 @@ public class TransactionController { @RequestHeader("Authorization") String authToken, @RequestParam(value = "limit", required = false) Integer limit, @RequestParam(value = "offset", required = false) Integer offset - ) { + ) { UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset); return ResponseEntity.ok(transactionEntities); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 0eaca18..e583c50 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -28,3 +28,16 @@ logging.level.org.springframework.security=DEBUG springdoc.swagger-ui.path=swagger springdoc.swagger-ui.try-it-out-enabled=true +# GitHub OAuth2 Configuration +spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK} +spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7} +spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github +spring.security.oauth2.client.registration.github.scope=user:email,read:user +spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize +spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token +spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user +spring.security.oauth2.client.provider.github.user-name-attribute=login + +# OAuth Success and Failure URLs +app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback + diff --git a/backend/src/main/resources/templates/email/verify.html b/backend/src/main/resources/templates/email/verify.html index b7dc2a6..7076a7c 100644 --- a/backend/src/main/resources/templates/email/verify.html +++ b/backend/src/main/resources/templates/email/verify.html @@ -123,7 +123,7 @@

Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:

diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index b4bf818..39c5b1c 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -33,6 +33,13 @@ export const routes: Routes = [ (m) => m.RecoverPasswordComponent ), }, + { + path: 'oauth2/callback/github', + loadComponent: () => + import('./feature/auth/oauth2/oauth2-callback.component').then( + (m) => m.OAuth2CallbackComponent + ), + }, { path: 'game/blackjack', loadComponent: () => import('./feature/game/blackjack/blackjack.component'), diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html index 04afd42..14a7d1e 100644 --- a/frontend/src/app/feature/auth/login/login.component.html +++ b/frontend/src/app/feature/auth/login/login.component.html @@ -83,7 +83,32 @@
-
+
+
+ ODER +
+
+ +
+ +
+ +

Passwort vergessen?

+ `, +}) +export class OAuth2CallbackComponent implements OnInit { + error: string | null = null; + + constructor( + private route: ActivatedRoute, + private router: Router, + private authService: AuthService + ) {} + + ngOnInit(): void { + // Check for code in URL params + this.route.queryParams.subscribe((params) => { + const code = params['code']; + + if (code) { + // Exchange GitHub code for a JWT token + this.authService.githubAuth(code).subscribe({ + next: () => { + // Redirect to home after successful authentication + this.router.navigate(['/home']); + }, + error: (err) => { + console.error('GitHub authentication error:', err); + this.error = err.error?.message || 'Authentication failed. Please try again.'; + console.log('Error details:', err); + + // Redirect back to landing page after showing error + setTimeout(() => { + this.router.navigate(['/']); + }, 3000); + }, + }); + } else { + this.error = 'Authentication failed. No authorization code received.'; + + // Redirect back to landing page after showing error + setTimeout(() => { + this.router.navigate(['/']); + }, 3000); + } + }); + } +} diff --git a/frontend/src/app/feature/auth/verify-email/verify-email.component.ts b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts index 4ccbbaa..6e04877 100644 --- a/frontend/src/app/feature/auth/verify-email/verify-email.component.ts +++ b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts @@ -13,7 +13,7 @@ export class VerifyEmailComponent implements OnInit { authService: AuthService = inject(AuthService); ngOnInit(): void { - const token = this.route.snapshot.queryParamMap.get('token'); + const token = this.route.snapshot.queryParamMap.get('email-token'); if (!token) { this.router.navigate(['/']); diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index f1cfbe9..081ad72 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable, tap } from 'rxjs'; -import { Router } from '@angular/router'; +import { Router, ActivatedRoute } from '@angular/router'; import { LoginRequest } from '../model/auth/LoginRequest'; import { RegisterRequest } from '../model/auth/RegisterRequest'; import { AuthResponse } from '../model/auth/AuthResponse'; @@ -17,20 +17,41 @@ const USER_KEY = 'user'; export class AuthService { private authUrl = `${environment.apiUrl}/auth`; private userUrl = `${environment.apiUrl}/users`; + private oauthUrl = `${environment.apiUrl}/oauth2`; userSubject: BehaviorSubject; constructor( private http: HttpClient, - private router: Router + private router: Router, + private route: ActivatedRoute ) { this.userSubject = new BehaviorSubject(this.getUserFromStorage()); + // Check for token in URL (OAuth callback) on initialization + this.route.queryParams.subscribe((params) => { + const token = params['token']; + if (token) { + this.handleOAuthCallback(token); + } + }); + if (this.getToken()) { this.loadCurrentUser(); } } + private handleOAuthCallback(token: string): void { + this.setToken(token); + this.loadCurrentUser(); + // Clean up the URL by removing the token + this.router.navigate([], { + relativeTo: this.route, + queryParams: {}, + replaceUrl: true, + }); + } + public get currentUserValue(): User | null { return this.userSubject.value; } @@ -48,6 +69,16 @@ export class AuthService { return this.http.post(`${this.authUrl}/register`, registerRequest); } + githubAuth(code: string): Observable { + return this.http.post(`${this.oauthUrl}/github/callback`, { code }).pipe( + tap((response) => { + console.log(response.token); + this.setToken(response.token); + this.loadCurrentUser(); + }) + ); + } + logout(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY);