Merge pull request 'feat(auth): add oauth2 using github (CAS-64)' (!208) from feat/github-oauth into main
All checks were successful
Release / Release (push) Successful in 1m20s
Release / Build Frontend Image (push) Successful in 53s
Release / Build Backend Image (push) Successful in 57s

Reviewed-on: #208
This commit is contained in:
Constantin Simonis 2025-05-21 09:03:43 +00:00
commit dae835cbfa
No known key found for this signature in database
GPG key ID: 944223E4D46B7412
42 changed files with 833 additions and 79 deletions

View file

@ -8,9 +8,6 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean; 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.mail.javamail.JavaMailSenderImpl;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;

View file

@ -112,7 +112,7 @@ public class BlackJackService {
dealCardToPlayer(game); dealCardToPlayer(game);
dealCardToSplitHand(game); dealCardToSplitHand(game);
return blackJackGameRepository.save(game); return processGameBasedOnState(game);
} }
private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) { private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) {

View file

@ -1,7 +1,6 @@
package de.szut.casino.config; package de.szut.casino.config;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;

View file

@ -18,12 +18,12 @@ public class WebConfig {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins(frontendHost) .allowedOrigins(frontendHost)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") .allowedHeaders("*")
.exposedHeaders("*") .exposedHeaders("*")
.allowCredentials(true) .allowCredentials(true)
.maxAge(3600); .maxAge(3600);
} }
}; };
} }

View file

@ -53,8 +53,8 @@ public class DepositController {
.build()) .build())
.setQuantity(1L) .setQuantity(1L)
.build()) .build())
.setSuccessUrl(frontendHost+"/home?success=true") .setSuccessUrl(frontendHost + "/home?success=true")
.setCancelUrl(frontendHost+"/home?success=false") .setCancelUrl(frontendHost + "/home?success=false")
.setMode(SessionCreateParams.Mode.PAYMENT) .setMode(SessionCreateParams.Mode.PAYMENT)
.build(); .build();

View file

@ -1,7 +1,5 @@
package de.szut.casino.exceptionHandling.exceptions; package de.szut.casino.exceptionHandling.exceptions;
import de.szut.casino.security.service.EmailService;
public class EmailNotVerifiedException extends Exception { public class EmailNotVerifiedException extends Exception {
public EmailNotVerifiedException() { public EmailNotVerifiedException() {
super("Email not verified"); super("Email not verified");

View file

@ -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);
}
}

View file

@ -1,6 +1,5 @@
package de.szut.casino.security; package de.szut.casino.security;
import de.szut.casino.exceptionHandling.ErrorDetails;
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException; import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
import de.szut.casino.security.dto.AuthResponseDto; import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.dto.LoginRequestDto; import de.szut.casino.security.dto.LoginRequestDto;
@ -15,12 +14,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthController { public class AuthController {
@Autowired @Autowired
private AuthService authService; private AuthService authService;
@ -38,11 +37,11 @@ public class AuthController {
@PostMapping("/verify") @PostMapping("/verify")
public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException { public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException {
if (authService.verifyEmail(token)) { if (authService.verifyEmail(token)) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/recover-password") @PostMapping("/recover-password")

View file

@ -19,23 +19,22 @@ public class CorsFilter implements Filter {
@Override @Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res; HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req; HttpServletRequest request = (HttpServletRequest) req;
// Allow requests from the frontend
response.setHeader("Access-Control-Allow-Origin", frontendHost); response.setHeader("Access-Control-Allow-Origin", frontendHost);
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*"); response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Expose-Headers", "*"); response.setHeader("Access-Control-Expose-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(HttpServletResponse.SC_OK);
return; return;
} }
chain.doFilter(req, res); chain.doFilter(req, res);
} }
} }

View file

@ -7,7 +7,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
@Override @Override
public AbstractAuthenticationToken convert(Jwt source) { public AbstractAuthenticationToken convert(Jwt source) {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();

View file

@ -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<AuthResponseDto> githubCallback(@RequestBody GithubCallbackDto githubCallbackDto) {
String code = githubCallbackDto.getCode();
AuthResponseDto response = githubService.processGithubCode(code);
return ResponseEntity.ok(response);
}
}

View file

@ -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<String, String> 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<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
"https://github.com/login/oauth/access_token",
HttpMethod.POST,
requestEntity,
Map.class
);
Map<String, Object> 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<String> userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders);
ResponseEntity<Map> userResponse = restTemplate.exchange(
"https://api.github.com/user",
HttpMethod.GET,
userInfoRequestEntity,
Map.class
);
Map<String, Object> userAttributes = userResponse.getBody();
HttpHeaders emailsHeaders = new HttpHeaders();
emailsHeaders.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> emailsRequestEntity = new HttpEntity<>(null, emailsHeaders);
ResponseEntity<List> emailsResponse = restTemplate.exchange(
"https://api.github.com/user/emails",
HttpMethod.GET,
emailsRequestEntity,
List.class
);
List<Map<String, Object>> emails = emailsResponse.getBody();
String email = null;
for (Map<String, Object> 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<UserEntity> 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);
}
}
}

View file

@ -38,13 +38,14 @@ public class SecurityConfig {
@Autowired @Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean @Bean
public DaoAuthenticationProvider authenticationProvider() { public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService); authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; return authProvider;
} }
@ -61,16 +62,16 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> { .authorizeHttpRequests(auth -> {
auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error").permitAll() auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error", "/oauth2/**").permitAll()
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated(); .anyRequest().authenticated();
}) })
.authenticationProvider(authenticationProvider()) .authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }

View file

@ -12,7 +12,7 @@ import lombok.Setter;
public class AuthResponseDto { public class AuthResponseDto {
private String token; private String token;
private String tokenType = "Bearer"; private String tokenType = "Bearer";
public AuthResponseDto(String token) { public AuthResponseDto(String token) {
this.token = token; this.token = token;
} }

View file

@ -0,0 +1,8 @@
package de.szut.casino.security.dto;
import lombok.Data;
@Data
public class GithubCallbackDto {
private String code;
}

View file

@ -13,7 +13,7 @@ import lombok.Setter;
public class LoginRequestDto { public class LoginRequestDto {
@NotBlank(message = "Username or email is required") @NotBlank(message = "Username or email is required")
private String usernameOrEmail; private String usernameOrEmail;
@NotBlank(message = "Password is required") @NotBlank(message = "Password is required")
private String password; private String password;
} }

View file

@ -35,11 +35,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(jwt, userDetails)) { if (jwtUtils.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()); userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken); SecurityContextHolder.getContext().setAuthentication(authToken);
} }

View file

@ -1,12 +1,16 @@
package de.szut.casino.security.jwt; package de.szut.casino.security.jwt;
import de.szut.casino.security.oauth2.UserPrincipal;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.security.Key; import java.security.Key;
@ -17,6 +21,7 @@ import java.util.function.Function;
@Component @Component
public class JwtUtils { public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${jwt.secret}") @Value("${jwt.secret}")
private String jwtSecret; private String jwtSecret;
@ -29,8 +34,26 @@ public class JwtUtils {
} }
public String generateToken(Authentication authentication) { public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); String subject = null;
return generateToken(userDetails.getUsername()); Map<String, Object> 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) { public String generateToken(String username) {
@ -40,6 +63,9 @@ public class JwtUtils {
private String createToken(Map<String, Object> claims, String subject) { private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date(); 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); Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder() return Jwts.builder()

View file

@ -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<UserEntity> 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);
}
}

View file

@ -0,0 +1,25 @@
package de.szut.casino.security.oauth2;
import java.util.Map;
public class GitHubOAuth2UserInfo extends OAuth2UserInfo {
public GitHubOAuth2UserInfo(Map<String, Object> 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");
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,20 @@
package de.szut.casino.security.oauth2;
import lombok.Getter;
import java.util.Map;
@Getter
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
}

View file

@ -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<String, Object> attributes) {
if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) {
return new GitHubOAuth2UserInfo(attributes);
} else {
throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
}
}
}

View file

@ -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<? extends GrantedAuthority> authorities;
@Setter
private Map<String, Object> attributes;
public UserPrincipal(Long id, String email, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.email = email;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(UserEntity user) {
List<GrantedAuthority> 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<String, Object> 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<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return String.valueOf(id);
}
}

View file

@ -19,7 +19,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.swing.text.html.Option;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;

View file

@ -20,17 +20,17 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Override @Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail); Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail);
if (user.isEmpty()) { if (user.isEmpty()) {
user = userRepository.findByEmail(usernameOrEmail); user = userRepository.findByEmail(usernameOrEmail);
} }
UserEntity userEntity = user.orElseThrow(() -> UserEntity userEntity = user.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail)); new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail));
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
userEntity.getUsername(), userEntity.getUsername(),
userEntity.getPassword(), userEntity.getPassword(),
new ArrayList<>()); new ArrayList<>());
} }
} }

View file

@ -0,0 +1,6 @@
package de.szut.casino.user;
public enum AuthProvider {
LOCAL,
GITHUB
}

View file

@ -1,9 +1,6 @@
package de.szut.casino.user; package de.szut.casino.user;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@ -18,15 +15,15 @@ public class UserEntity {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@Column(unique = true) @Column(unique = true)
private String email; private String email;
@Column(unique = true) @Column(unique = true)
private String username; private String username;
private String password; private String password;
@Column(precision = 19, scale = 2) @Column(precision = 19, scale = 2)
private BigDecimal balance; private BigDecimal balance;
@ -36,12 +33,27 @@ public class UserEntity {
private String passwordResetToken; 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) { public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
this.email = email; this.email = email;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.balance = balance; this.balance = balance;
this.verificationToken = verificationToken; 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) { public void addBalance(BigDecimal amountToAdd) {

View file

@ -5,7 +5,7 @@ import org.springframework.stereotype.Service;
@Service @Service
public class UserMappingService { public class UserMappingService {
public GetUserDto mapToGetUserDto(UserEntity user) { public GetUserDto mapToGetUserDto(UserEntity user) {
return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance()); return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance());
} }

View file

@ -9,11 +9,13 @@ import java.util.Optional;
@Service @Service
public interface UserRepository extends JpaRepository<UserEntity, Long> { public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username); Optional<UserEntity> findByUsername(String username);
Optional<UserEntity> findByEmail(String email); Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByProviderId(String providerId);
boolean existsByUsername(String username); boolean existsByUsername(String username);
boolean existsByEmail(String email); boolean existsByEmail(String email);
@Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1") @Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1")

View file

@ -27,15 +27,15 @@ public class UserService {
if (userRepository.existsByEmail(createUserDto.getEmail())) { if (userRepository.existsByEmail(createUserDto.getEmail())) {
throw new EntityExistsException("Email is already in use"); throw new EntityExistsException("Email is already in use");
} }
UserEntity user = new UserEntity( UserEntity user = new UserEntity(
createUserDto.getEmail(), createUserDto.getEmail(),
createUserDto.getUsername(), createUserDto.getUsername(),
passwordEncoder.encode(createUserDto.getPassword()), passwordEncoder.encode(createUserDto.getPassword()),
BigDecimal.valueOf(100), BigDecimal.valueOf(100),
RandomStringUtils.randomAlphanumeric(64) RandomStringUtils.randomAlphanumeric(64)
); );
return userRepository.save(user); return userRepository.save(user);
} }
@ -50,7 +50,7 @@ public class UserService {
} }
public void saveUser(UserEntity user) { public void saveUser(UserEntity user) {
userRepository.save(user); userRepository.save(user);
} }
public boolean isVerified(String usernameOrEmail) { public boolean isVerified(String usernameOrEmail) {

View file

@ -16,11 +16,11 @@ public class CreateUserDto {
@NotBlank(message = "Email is required") @NotBlank(message = "Email is required")
@Email(message = "Email should be valid") @Email(message = "Email should be valid")
private String email; private String email;
@NotBlank(message = "Username is required") @NotBlank(message = "Username is required")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username; private String username;
@NotBlank(message = "Password is required") @NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters") @Size(min = 6, message = "Password must be at least 6 characters")
private String password; private String password;

View file

@ -19,7 +19,7 @@ public class TransactionController {
@RequestHeader("Authorization") String authToken, @RequestHeader("Authorization") String authToken,
@RequestParam(value = "limit", required = false) Integer limit, @RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "offset", required = false) Integer offset @RequestParam(value = "offset", required = false) Integer offset
) { ) {
UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset); UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset);
return ResponseEntity.ok(transactionEntities); return ResponseEntity.ok(transactionEntities);

View file

@ -28,3 +28,16 @@ logging.level.org.springframework.security=DEBUG
springdoc.swagger-ui.path=swagger springdoc.swagger-ui.path=swagger
springdoc.swagger-ui.try-it-out-enabled=true 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

View file

@ -123,7 +123,7 @@
<p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p> <p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p>
<div style="text-align: center;"> <div style="text-align: center;">
<a href="${feUrl}/verify?token=${token}" class="button">E-Mail bestätigen</a> <a href="${feUrl}/verify?email-token=${token}" class="button">E-Mail bestätigen</a>
</div> </div>
<div class="info-box"> <div class="info-box">

View file

@ -33,6 +33,13 @@ export const routes: Routes = [
(m) => m.RecoverPasswordComponent (m) => m.RecoverPasswordComponent
), ),
}, },
{
path: 'oauth2/callback/github',
loadComponent: () =>
import('./feature/auth/oauth2/oauth2-callback.component').then(
(m) => m.OAuth2CallbackComponent
),
},
{ {
path: 'game/blackjack', path: 'game/blackjack',
loadComponent: () => import('./feature/game/blackjack/blackjack.component'), loadComponent: () => import('./feature/game/blackjack/blackjack.component'),

View file

@ -83,7 +83,32 @@
</div> </div>
</form> </form>
<div class="mt-6 text-center"> <div class="my-4 flex items-center">
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
<span class="px-3 text-xs text-text-secondary">ODER</span>
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
</div>
<div class="mb-4">
<button
(click)="loginWithGithub()"
class="w-full py-2.5 px-4 rounded flex items-center justify-center bg-gray-800 hover:bg-gray-700 text-white transition-colors"
>
<svg
class="h-5 w-5 mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"
/>
</svg>
Mit GitHub anmelden
</button>
</div>
<div class="text-center">
<p class="text-sm text-text-secondary"> <p class="text-sm text-text-secondary">
Passwort vergessen? Passwort vergessen?
<button <button

View file

@ -4,6 +4,7 @@ import { Router } from '@angular/router';
import { LoginRequest } from '../../../model/auth/LoginRequest'; import { LoginRequest } from '../../../model/auth/LoginRequest';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { environment } from '@environments/environment';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -65,6 +66,11 @@ export class LoginComponent {
}); });
} }
loginWithGithub(): void {
this.isLoading.set(true);
window.location.href = `${environment.apiUrl}/oauth2/github/authorize`;
}
switchToForgotPassword() { switchToForgotPassword() {
this.forgotPassword.emit(); this.forgotPassword.emit();
} }

View file

@ -0,0 +1,64 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '@service/auth.service';
@Component({
selector: 'app-oauth2-callback',
standalone: true,
imports: [CommonModule],
template: `
<div class="min-h-screen bg-deep-blue flex items-center justify-center">
<div class="text-center">
<h2 class="text-2xl font-bold text-white mb-4">Finishing authentication...</h2>
<div
class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald mx-auto"
></div>
<p *ngIf="error" class="mt-4 text-accent-red">{{ error }}</p>
</div>
</div>
`,
})
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);
}
});
}
}

View file

@ -13,7 +13,7 @@ export class VerifyEmailComponent implements OnInit {
authService: AuthService = inject(AuthService); authService: AuthService = inject(AuthService);
ngOnInit(): void { ngOnInit(): void {
const token = this.route.snapshot.queryParamMap.get('token'); const token = this.route.snapshot.queryParamMap.get('email-token');
if (!token) { if (!token) {
this.router.navigate(['/']); this.router.navigate(['/']);

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs'; import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { LoginRequest } from '../model/auth/LoginRequest'; import { LoginRequest } from '../model/auth/LoginRequest';
import { RegisterRequest } from '../model/auth/RegisterRequest'; import { RegisterRequest } from '../model/auth/RegisterRequest';
import { AuthResponse } from '../model/auth/AuthResponse'; import { AuthResponse } from '../model/auth/AuthResponse';
@ -17,20 +17,41 @@ const USER_KEY = 'user';
export class AuthService { export class AuthService {
private authUrl = `${environment.apiUrl}/auth`; private authUrl = `${environment.apiUrl}/auth`;
private userUrl = `${environment.apiUrl}/users`; private userUrl = `${environment.apiUrl}/users`;
private oauthUrl = `${environment.apiUrl}/oauth2`;
userSubject: BehaviorSubject<User | null>; userSubject: BehaviorSubject<User | null>;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private router: Router private router: Router,
private route: ActivatedRoute
) { ) {
this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage()); this.userSubject = new BehaviorSubject<User | null>(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()) { if (this.getToken()) {
this.loadCurrentUser(); 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 { public get currentUserValue(): User | null {
return this.userSubject.value; return this.userSubject.value;
} }
@ -48,6 +69,16 @@ export class AuthService {
return this.http.post<User>(`${this.authUrl}/register`, registerRequest); return this.http.post<User>(`${this.authUrl}/register`, registerRequest);
} }
githubAuth(code: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.oauthUrl}/github/callback`, { code }).pipe(
tap((response) => {
console.log(response.token);
this.setToken(response.token);
this.loadCurrentUser();
})
);
}
logout(): void { logout(): void {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY); localStorage.removeItem(USER_KEY);