feat: implement authentication with JWT and user management
This commit is contained in:
		
					parent
					
						
							
								c4c762cafe
							
						
					
				
			
			
				commit
				
					
						35d8fbaea0
					
				
			
		
					 42 changed files with 989 additions and 397 deletions
				
			
		|  | @ -51,6 +51,9 @@ dependencies { | |||
|     implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.5") | ||||
|     runtimeOnly("org.postgresql:postgresql") | ||||
|     implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8") | ||||
|     implementation("io.jsonwebtoken:jjwt-api:0.11.5") | ||||
|     runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") | ||||
|     runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") | ||||
| } | ||||
| 
 | ||||
| tasks.withType<Test> { | ||||
|  |  | |||
|  | @ -30,10 +30,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @GetMapping("/blackjack/{id}") | ||||
|     public ResponseEntity<Object> getGame(@PathVariable Long id, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  | @ -47,10 +47,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @PostMapping("/blackjack/{id}/hit") | ||||
|     public ResponseEntity<Object> hit(@PathVariable Long id, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  | @ -64,10 +64,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @PostMapping("/blackjack/{id}/stand") | ||||
|     public ResponseEntity<Object> stand(@PathVariable Long id, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  | @ -81,10 +81,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @PostMapping("/blackjack/{id}/doubleDown") | ||||
|     public ResponseEntity<Object> doubleDown(@PathVariable Long id, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  | @ -98,10 +98,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @PostMapping("/blackjack/{id}/split") | ||||
|     public ResponseEntity<Object> split(@PathVariable Long id, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  | @ -115,10 +115,10 @@ public class BlackJackGameController { | |||
| 
 | ||||
|     @PostMapping("/blackjack/start") | ||||
|     public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  |  | |||
							
								
								
									
										30
									
								
								backend/src/main/java/de/szut/casino/config/WebConfig.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/src/main/java/de/szut/casino/config/WebConfig.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| package de.szut.casino.config; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.web.servlet.config.annotation.CorsRegistry; | ||||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||||
| 
 | ||||
| @Configuration | ||||
| public class WebConfig { | ||||
| 
 | ||||
|     @Value("${app.frontend-host}") | ||||
|     private String frontendHost; | ||||
| 
 | ||||
|     @Bean | ||||
|     public WebMvcConfigurer corsConfigurer() { | ||||
|         return new WebMvcConfigurer() { | ||||
|             @Override | ||||
|             public void addCorsMappings(CorsRegistry registry) { | ||||
|                 registry.addMapping("/**") | ||||
|                     .allowedOrigins(frontendHost) | ||||
|                     .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") | ||||
|                     .allowedHeaders("*") | ||||
|                     .exposedHeaders("*") | ||||
|                     .allowCredentials(true) | ||||
|                     .maxAge(3600); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | @ -50,7 +50,7 @@ public class DepositController { | |||
|         Stripe.apiKey = stripeKey; | ||||
| 
 | ||||
|         KeycloakUserDto userData = getAuthentikUserInfo(token); | ||||
|         Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub()); | ||||
|         Optional<UserEntity> optionalUserEntity = this.userRepository.findByEmail(userData.getSub()); | ||||
| 
 | ||||
|         SessionCreateParams params = SessionCreateParams.builder() | ||||
|                 .addLineItem(SessionCreateParams.LineItem.builder() | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; | |||
| 
 | ||||
| @ResponseStatus(value = HttpStatus.NOT_FOUND) | ||||
| public class UserNotFoundException extends RuntimeException { | ||||
|     public UserNotFoundException() { | ||||
|         super("user not found"); | ||||
|     public UserNotFoundException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -37,9 +37,9 @@ public class LootBoxController { | |||
| 
 | ||||
|         LootBoxEntity lootBox = optionalLootBox.get(); | ||||
| 
 | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  |  | |||
|  | @ -0,0 +1,31 @@ | |||
| package de.szut.casino.security; | ||||
| 
 | ||||
| import de.szut.casino.security.dto.AuthResponseDto; | ||||
| import de.szut.casino.security.dto.LoginRequestDto; | ||||
| import de.szut.casino.security.service.AuthService; | ||||
| import de.szut.casino.user.dto.CreateUserDto; | ||||
| import de.szut.casino.user.dto.GetUserDto; | ||||
| import jakarta.validation.Valid; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| 
 | ||||
| @RestController | ||||
| @RequestMapping("/api/auth") | ||||
| public class AuthController { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private AuthService authService; | ||||
| 
 | ||||
|     @PostMapping("/login") | ||||
|     public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) { | ||||
|         AuthResponseDto response = authService.login(loginRequest); | ||||
|         return ResponseEntity.ok(response); | ||||
|     } | ||||
| 
 | ||||
|     @PostMapping("/register") | ||||
|     public ResponseEntity<GetUserDto> registerUser(@Valid @RequestBody CreateUserDto signUpRequest) { | ||||
|         GetUserDto response = authService.register(signUpRequest); | ||||
|         return ResponseEntity.ok(response); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,42 @@ | |||
| package de.szut.casino.security; | ||||
| 
 | ||||
| import jakarta.servlet.*; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.core.Ordered; | ||||
| import org.springframework.core.annotation.Order; | ||||
| import org.springframework.stereotype.Component; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| @Component | ||||
| @Order(Ordered.HIGHEST_PRECEDENCE) | ||||
| public class CorsFilter implements Filter { | ||||
| 
 | ||||
|     @Value("${app.frontend-host}") | ||||
|     private String frontendHost; | ||||
| 
 | ||||
|     @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); | ||||
|     } | ||||
| } | ||||
|  | @ -1,12 +1,22 @@ | |||
| package de.szut.casino.security; | ||||
| 
 | ||||
| import de.szut.casino.security.jwt.JwtAuthenticationFilter; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.security.config.Customizer; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||||
| import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; | ||||
| import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; | ||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||||
| import org.springframework.security.config.http.SessionCreationPolicy; | ||||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.security.web.SecurityFilterChain; | ||||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||||
| import org.springframework.web.cors.CorsConfiguration; | ||||
| import org.springframework.web.cors.CorsConfigurationSource; | ||||
| import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||||
|  | @ -16,23 +26,51 @@ import java.util.List; | |||
| 
 | ||||
| @Configuration | ||||
| @EnableWebSecurity | ||||
| @EnableMethodSecurity | ||||
| public class SecurityConfig { | ||||
| 
 | ||||
|     @Value("${app.frontend-host}") | ||||
|     private String frontendHost; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserDetailsService userDetailsService; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private JwtAuthenticationFilter jwtAuthenticationFilter; | ||||
| 
 | ||||
|     @Bean | ||||
|     public DaoAuthenticationProvider authenticationProvider() { | ||||
|         DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); | ||||
|          | ||||
|         authProvider.setUserDetailsService(userDetailsService); | ||||
|         authProvider.setPasswordEncoder(passwordEncoder()); | ||||
|          | ||||
|         return authProvider; | ||||
|     } | ||||
| 
 | ||||
|     @Bean | ||||
|     public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { | ||||
|         return authConfig.getAuthenticationManager(); | ||||
|     } | ||||
| 
 | ||||
|     @Bean | ||||
|     public PasswordEncoder passwordEncoder() { | ||||
|         return new BCryptPasswordEncoder(); | ||||
|     } | ||||
| 
 | ||||
|     @Bean | ||||
|     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||||
|         http | ||||
|             .cors(Customizer.withDefaults()) | ||||
|             .cors(cors -> cors.configurationSource(corsConfigurationSource())) | ||||
|             .csrf(csrf -> csrf.disable()) | ||||
|             .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||||
|             .authorizeHttpRequests(auth -> { | ||||
|                 auth.requestMatchers("/webhook", "/swagger/**", "/swagger-ui/**", "/health").permitAll() | ||||
|                 auth.requestMatchers("/api/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error").permitAll() | ||||
|                     .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() | ||||
|                     .anyRequest().authenticated(); | ||||
|                 }) | ||||
|                 .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> | ||||
|                     jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter()) | ||||
|                 )); | ||||
|             .authenticationProvider(authenticationProvider()) | ||||
|             .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||||
| 
 | ||||
|         return http.build(); | ||||
|     } | ||||
|  | @ -42,9 +80,10 @@ public class SecurityConfig { | |||
|         CorsConfiguration configuration = new CorsConfiguration(); | ||||
|         configuration.setAllowedOrigins(List.of(this.frontendHost)); | ||||
|         configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); | ||||
|         configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Access-Control-Allow-Origin")); | ||||
|         configuration.setExposedHeaders(List.of("x-auth-token")); | ||||
|         configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Headers", "x-auth-token")); | ||||
|         configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type", "x-auth-token", "Access-Control-Allow-Origin", "Access-Control-Allow-Methods", "Access-Control-Allow-Headers")); | ||||
|         configuration.setAllowCredentials(true); | ||||
|         configuration.setMaxAge(3600L); | ||||
|         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | ||||
|         source.registerCorsConfiguration("/**", configuration); | ||||
|         return source; | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| package de.szut.casino.security.dto; | ||||
| 
 | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.Setter; | ||||
| 
 | ||||
| @Getter | ||||
| @Setter | ||||
| @NoArgsConstructor | ||||
| @AllArgsConstructor | ||||
| public class AuthResponseDto { | ||||
|     private String token; | ||||
|     private String tokenType = "Bearer"; | ||||
|      | ||||
|     public AuthResponseDto(String token) { | ||||
|         this.token = token; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| package de.szut.casino.security.dto; | ||||
| 
 | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.Setter; | ||||
| 
 | ||||
| @Getter | ||||
| @Setter | ||||
| @NoArgsConstructor | ||||
| @AllArgsConstructor | ||||
| public class LoginRequestDto { | ||||
|     @NotBlank(message = "Username or email is required") | ||||
|     private String usernameOrEmail; | ||||
|      | ||||
|     @NotBlank(message = "Password is required") | ||||
|     private String password; | ||||
| } | ||||
|  | @ -0,0 +1,64 @@ | |||
| package de.szut.casino.security.jwt; | ||||
| 
 | ||||
| import jakarta.servlet.FilterChain; | ||||
| import jakarta.servlet.ServletException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.util.StringUtils; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| @Component | ||||
| public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private JwtUtils jwtUtils; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserDetailsService userDetailsService; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws ServletException, IOException { | ||||
|         try { | ||||
|             String jwt = parseJwt(request); | ||||
|             if (jwt != null) { | ||||
|                 String username = jwtUtils.extractUsername(jwt); | ||||
| 
 | ||||
|                 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); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             logger.error("Cannot set user authentication: {}", e); | ||||
|         } | ||||
| 
 | ||||
|         filterChain.doFilter(request, response); | ||||
|     } | ||||
| 
 | ||||
|     private String parseJwt(HttpServletRequest request) { | ||||
|         String headerAuth = request.getHeader("Authorization"); | ||||
| 
 | ||||
|         if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { | ||||
|             return headerAuth.substring(7); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,83 @@ | |||
| package de.szut.casino.security.jwt; | ||||
| 
 | ||||
| import io.jsonwebtoken.Claims; | ||||
| import io.jsonwebtoken.Jwts; | ||||
| import io.jsonwebtoken.SignatureAlgorithm; | ||||
| import io.jsonwebtoken.security.Keys; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.stereotype.Component; | ||||
| 
 | ||||
| import java.security.Key; | ||||
| import java.util.Date; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.function.Function; | ||||
| 
 | ||||
| @Component | ||||
| public class JwtUtils { | ||||
| 
 | ||||
|     @Value("${jwt.secret}") | ||||
|     private String jwtSecret; | ||||
| 
 | ||||
|     @Value("${jwt.expiration.ms}") | ||||
|     private int jwtExpirationMs; | ||||
| 
 | ||||
|     private Key getSigningKey() { | ||||
|         return Keys.hmacShaKeyFor(jwtSecret.getBytes()); | ||||
|     } | ||||
| 
 | ||||
|     public String generateToken(Authentication authentication) { | ||||
|         UserDetails userDetails = (UserDetails) authentication.getPrincipal(); | ||||
|         return generateToken(userDetails.getUsername()); | ||||
|     } | ||||
| 
 | ||||
|     public String generateToken(String username) { | ||||
|         Map<String, Object> claims = new HashMap<>(); | ||||
|         return createToken(claims, username); | ||||
|     } | ||||
| 
 | ||||
|     private String createToken(Map<String, Object> claims, String subject) { | ||||
|         Date now = new Date(); | ||||
|         Date expiryDate = new Date(now.getTime() + jwtExpirationMs); | ||||
| 
 | ||||
|         return Jwts.builder() | ||||
|                 .setClaims(claims) | ||||
|                 .setSubject(subject) | ||||
|                 .setIssuedAt(now) | ||||
|                 .setExpiration(expiryDate) | ||||
|                 .signWith(getSigningKey(), SignatureAlgorithm.HS256) | ||||
|                 .compact(); | ||||
|     } | ||||
| 
 | ||||
|     public String extractUsername(String token) { | ||||
|         return extractClaim(token, Claims::getSubject); | ||||
|     } | ||||
| 
 | ||||
|     public Date extractExpiration(String token) { | ||||
|         return extractClaim(token, Claims::getExpiration); | ||||
|     } | ||||
| 
 | ||||
|     public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { | ||||
|         final Claims claims = extractAllClaims(token); | ||||
|         return claimsResolver.apply(claims); | ||||
|     } | ||||
| 
 | ||||
|     private Claims extractAllClaims(String token) { | ||||
|         return Jwts.parserBuilder() | ||||
|                 .setSigningKey(getSigningKey()) | ||||
|                 .build() | ||||
|                 .parseClaimsJws(token) | ||||
|                 .getBody(); | ||||
|     } | ||||
| 
 | ||||
|     private Boolean isTokenExpired(String token) { | ||||
|         return extractExpiration(token).before(new Date()); | ||||
|     } | ||||
| 
 | ||||
|     public Boolean validateToken(String token, UserDetails userDetails) { | ||||
|         final String username = extractUsername(token); | ||||
|         return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| package de.szut.casino.security.service; | ||||
| 
 | ||||
| import de.szut.casino.security.dto.AuthResponseDto; | ||||
| import de.szut.casino.security.dto.LoginRequestDto; | ||||
| import de.szut.casino.security.jwt.JwtUtils; | ||||
| import de.szut.casino.user.UserEntity; | ||||
| import de.szut.casino.user.UserService; | ||||
| import de.szut.casino.user.dto.CreateUserDto; | ||||
| import de.szut.casino.user.dto.GetUserDto; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.stereotype.Service; | ||||
| 
 | ||||
| @Service | ||||
| public class AuthService { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private AuthenticationManager authenticationManager; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private JwtUtils jwtUtils; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserService userService; | ||||
| 
 | ||||
|     public AuthResponseDto login(LoginRequestDto loginRequest) { | ||||
|         Authentication authentication = authenticationManager.authenticate( | ||||
|                 new UsernamePasswordAuthenticationToken( | ||||
|                         loginRequest.getUsernameOrEmail(), | ||||
|                         loginRequest.getPassword())); | ||||
| 
 | ||||
|         SecurityContextHolder.getContext().setAuthentication(authentication); | ||||
|         String jwt = jwtUtils.generateToken(authentication); | ||||
|          | ||||
|         return new AuthResponseDto(jwt); | ||||
|     } | ||||
| 
 | ||||
|     public GetUserDto register(CreateUserDto signUpRequest) { | ||||
|         UserEntity user = userService.createUser(signUpRequest); | ||||
|         return new GetUserDto( | ||||
|                 user.getId(), | ||||
|                 user.getEmail(), | ||||
|                 user.getUsername(), | ||||
|                 user.getBalance() | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,48 @@ | |||
| package de.szut.casino.security.service; | ||||
| 
 | ||||
| import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; | ||||
| import de.szut.casino.user.UserEntity; | ||||
| import de.szut.casino.user.UserRepository; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||||
| import org.springframework.stereotype.Service; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| @Service | ||||
| public class UserDetailsServiceImpl implements UserDetailsService { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserRepository userRepository; | ||||
| 
 | ||||
|     @Override | ||||
|     public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { | ||||
|         Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail); | ||||
|          | ||||
|         if (user.isEmpty()) { | ||||
|             user = userRepository.findByEmail(usernameOrEmail); | ||||
|         } | ||||
|          | ||||
|         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(),  | ||||
|                 new ArrayList<>()); | ||||
|     } | ||||
|      | ||||
|     public UserEntity getUserByUsernameOrEmail(String usernameOrEmail) { | ||||
|         Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail); | ||||
|          | ||||
|         if (user.isEmpty()) { | ||||
|             user = userRepository.findByEmail(usernameOrEmail); | ||||
|         } | ||||
|          | ||||
|         return user.orElseThrow(() ->  | ||||
|                 new UserNotFoundException("User not found with username or email: " + usernameOrEmail)); | ||||
|     } | ||||
| } | ||||
|  | @ -30,10 +30,10 @@ public class SlotController { | |||
| 
 | ||||
|     @PostMapping("/slots/spin") | ||||
|     public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(token); | ||||
|         Optional<UserEntity> optionalUser = userService.getCurrentUser(); | ||||
| 
 | ||||
|         if (optionalUser.isEmpty()) { | ||||
|             throw new UserNotFoundException(); | ||||
|             throw new UserNotFoundException("User not found"); | ||||
|         } | ||||
| 
 | ||||
|         UserEntity user = optionalUser.get(); | ||||
|  |  | |||
|  | @ -1,43 +1,41 @@ | |||
| package de.szut.casino.user; | ||||
| 
 | ||||
| import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; | ||||
| import de.szut.casino.user.dto.CreateUserDto; | ||||
| import de.szut.casino.user.dto.GetUserDto; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| 
 | ||||
| @Slf4j | ||||
| @RestController | ||||
| @CrossOrigin | ||||
| @RequestMapping("/api/users") | ||||
| public class UserController { | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserService userService; | ||||
| 
 | ||||
|     @PostMapping("/user") | ||||
|     public ResponseEntity<?> createUser(@RequestBody @Valid CreateUserDto userData) { | ||||
|         if (userService.exists(userData.getAuthentikId())) { | ||||
|             HttpHeaders headers = new HttpHeaders(); | ||||
|             headers.add("Location", "/user"); | ||||
|     @Autowired | ||||
|     private UserMappingService userMappingService; | ||||
| 
 | ||||
|             return new ResponseEntity<>(headers, HttpStatus.FOUND); | ||||
|         } | ||||
| 
 | ||||
|         return ResponseEntity.ok(userService.createUser(userData)); | ||||
|     @GetMapping("/me") | ||||
|     public ResponseEntity<GetUserDto> getCurrentUser() { | ||||
|         return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser().orElseThrow())); | ||||
|     } | ||||
| 
 | ||||
|     @GetMapping("/user") | ||||
|     public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) { | ||||
|         GetUserDto userData = userService.getCurrentUserAsDto(token); | ||||
| 
 | ||||
|         if (userData == null) { | ||||
|             throw new UserNotFoundException(); | ||||
|         } | ||||
| 
 | ||||
|         return ResponseEntity.ok(userData); | ||||
|      | ||||
|     @GetMapping("/current") | ||||
|     public ResponseEntity<GetUserDto> getCurrentUserAlternative() { | ||||
|         return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser().orElseThrow())); | ||||
|     } | ||||
|      | ||||
|     @GetMapping("/{id}") | ||||
|     public ResponseEntity<GetUserDto> getUserById(@PathVariable Long id) { | ||||
|         return ResponseEntity.ok(userService.getUserById(id)); | ||||
|     } | ||||
|      | ||||
|     @GetMapping("/username/{username}") | ||||
|     public ResponseEntity<GetUserDto> getUserByUsername(@PathVariable String username) { | ||||
|         return ResponseEntity.ok(userService.getUserByUsername(username)); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -18,16 +18,22 @@ public class UserEntity { | |||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|      | ||||
|     @Column(unique = true) | ||||
|     private String email; | ||||
|      | ||||
|     @Column(unique = true) | ||||
|     private String authentikId; | ||||
|     private String username; | ||||
| 
 | ||||
|      | ||||
|     private String password; | ||||
|      | ||||
|     @Column(precision = 19, scale = 2) | ||||
|     private BigDecimal balance; | ||||
| 
 | ||||
|     public UserEntity(String authentikId, String username, BigDecimal balance) { | ||||
|         this.authentikId = authentikId; | ||||
|     public UserEntity(String email, String username, String password, BigDecimal balance) { | ||||
|         this.email = email; | ||||
|         this.username = username; | ||||
|         this.password = password; | ||||
|         this.balance = balance; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,18 +2,17 @@ package de.szut.casino.user; | |||
| 
 | ||||
| import de.szut.casino.user.dto.CreateUserDto; | ||||
| import de.szut.casino.user.dto.GetUserDto; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.stereotype.Service; | ||||
| 
 | ||||
| import java.math.BigDecimal; | ||||
| 
 | ||||
| @Service | ||||
| public class UserMappingService { | ||||
|      | ||||
|     public GetUserDto mapToGetUserDto(UserEntity user) { | ||||
|         return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance()); | ||||
|     } | ||||
| 
 | ||||
|     public UserEntity mapToUserEntity(CreateUserDto createUserDto) { | ||||
|         return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO); | ||||
|         return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| package de.szut.casino.user; | ||||
| 
 | ||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||
| import org.springframework.data.jpa.repository.Query; | ||||
| import org.springframework.stereotype.Service; | ||||
| 
 | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| @Service | ||||
| public interface UserRepository extends JpaRepository<UserEntity, Long> { | ||||
|     @Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1") | ||||
|     Optional<UserEntity> findOneByAuthentikId(String authentikId); | ||||
| 
 | ||||
|     boolean existsByAuthentikId(String authentikId); | ||||
|     Optional<UserEntity> findByUsername(String username); | ||||
|      | ||||
|     Optional<UserEntity> findByEmail(String email); | ||||
|      | ||||
|     boolean existsByUsername(String username); | ||||
|      | ||||
|     boolean existsByEmail(String email); | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,14 @@ | |||
| package de.szut.casino.user; | ||||
| 
 | ||||
| import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; | ||||
| import de.szut.casino.user.dto.CreateUserDto; | ||||
| import de.szut.casino.user.dto.GetUserDto; | ||||
| import de.szut.casino.user.dto.KeycloakUserDto; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.HttpEntity; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.http.HttpMethod; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| 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.Optional; | ||||
| 
 | ||||
| @Service | ||||
|  | @ -18,64 +16,69 @@ public class UserService { | |||
|     @Autowired | ||||
|     private UserRepository userRepository; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private RestTemplate http; | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserMappingService mappingService; | ||||
|      | ||||
|     @Autowired | ||||
|     private PasswordEncoder passwordEncoder; | ||||
| 
 | ||||
|     public UserEntity createUser(CreateUserDto createUserDto) { | ||||
|         UserEntity user = mappingService.mapToUserEntity(createUserDto); | ||||
|         userRepository.save(user); | ||||
| 
 | ||||
|         return user; | ||||
|     } | ||||
| 
 | ||||
|     public GetUserDto getUser(String authentikId) { | ||||
|         Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(authentikId); | ||||
| 
 | ||||
|         return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); | ||||
|     } | ||||
| 
 | ||||
|     public GetUserDto getCurrentUserAsDto(String token) { | ||||
|         KeycloakUserDto userData = getAuthentikUserInfo(token); | ||||
| 
 | ||||
|         if (userData == null) { | ||||
|             return null; | ||||
|         if (userRepository.existsByUsername(createUserDto.getUsername())) { | ||||
|             throw new IllegalArgumentException("Username is already taken"); | ||||
|         } | ||||
|         Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(userData.getSub()); | ||||
| 
 | ||||
|         return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); | ||||
|     } | ||||
| 
 | ||||
|     public Optional<UserEntity> getCurrentUser(String token) { | ||||
|         KeycloakUserDto userData = getAuthentikUserInfo(token); | ||||
| 
 | ||||
|         if (userData == null) { | ||||
|             return Optional.empty(); | ||||
|         if (userRepository.existsByEmail(createUserDto.getEmail())) { | ||||
|             throw new IllegalArgumentException("Email is already in use"); | ||||
|         } | ||||
|         return this.userRepository.findOneByAuthentikId(userData.getSub()); | ||||
|          | ||||
|         UserEntity user = new UserEntity( | ||||
|             createUserDto.getEmail(), | ||||
|             createUserDto.getUsername(), | ||||
|             passwordEncoder.encode(createUserDto.getPassword()), | ||||
|             BigDecimal.valueOf(1000)  // Starting balance | ||||
|         ); | ||||
|          | ||||
|         return userRepository.save(user); | ||||
|     } | ||||
| 
 | ||||
|     private KeycloakUserDto getAuthentikUserInfo(String token) { | ||||
|         try { | ||||
|             HttpHeaders headers = new HttpHeaders(); | ||||
|             headers.set("Authorization", token); | ||||
|             ResponseEntity<KeycloakUserDto> response = this.http.exchange( | ||||
|                 "https://oauth.simonis.lol/application/o/userinfo/",  | ||||
|                 HttpMethod.GET,  | ||||
|                 new HttpEntity<>(headers),  | ||||
|                 KeycloakUserDto.class | ||||
|             ); | ||||
|              | ||||
|             return response.getBody(); | ||||
|         } catch (Exception e) { | ||||
|             System.err.println("Error fetching user info from Authentik: " + e.getMessage()); | ||||
|             return null; | ||||
|         } | ||||
|     public GetUserDto getUserById(Long id) { | ||||
|         UserEntity user = userRepository.findById(id) | ||||
|             .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id)); | ||||
|          | ||||
|         return mappingService.mapToGetUserDto(user); | ||||
|     } | ||||
|      | ||||
|     public GetUserDto getUserByUsername(String username) { | ||||
|         UserEntity user = userRepository.findByUsername(username) | ||||
|             .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username)); | ||||
|          | ||||
|         return mappingService.mapToGetUserDto(user); | ||||
|     } | ||||
|      | ||||
|     public GetUserDto getUserByEmail(String email) { | ||||
|         UserEntity user = userRepository.findByEmail(email) | ||||
|             .orElseThrow(() -> new UserNotFoundException("User not found with email: " + email)); | ||||
|          | ||||
|         return mappingService.mapToGetUserDto(user); | ||||
|     } | ||||
| 
 | ||||
|     public boolean exists(String authentikId) { | ||||
|         return userRepository.existsByAuthentikId(authentikId); | ||||
|     public Optional<UserEntity> getCurrentUser() { | ||||
|         String username = SecurityContextHolder.getContext().getAuthentication().getName(); | ||||
| 
 | ||||
|         return userRepository.findByUsername(username); | ||||
|     } | ||||
|      | ||||
|     public UserEntity getCurrentUserEntity() { | ||||
|         String username = SecurityContextHolder.getContext().getAuthentication().getName(); | ||||
|         return userRepository.findByUsername(username) | ||||
|             .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username)); | ||||
|     } | ||||
|      | ||||
|     public boolean existsByUsername(String username) { | ||||
|         return userRepository.existsByUsername(username); | ||||
|     } | ||||
|      | ||||
|     public boolean existsByEmail(String email) { | ||||
|         return userRepository.existsByEmail(email); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| package de.szut.casino.user.dto; | ||||
| 
 | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | @ -10,6 +13,15 @@ import lombok.Setter; | |||
| @AllArgsConstructor | ||||
| @NoArgsConstructor | ||||
| public class CreateUserDto { | ||||
|     private String authentikId; | ||||
|     @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; | ||||
| } | ||||
|  |  | |||
|  | @ -12,7 +12,8 @@ import java.math.BigDecimal; | |||
| @AllArgsConstructor | ||||
| @NoArgsConstructor | ||||
| public class GetUserDto { | ||||
|     private String authentikId; | ||||
|     private Long id; | ||||
|     private String email; | ||||
|     private String username; | ||||
|     private BigDecimal balance; | ||||
| } | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ public class GetTransactionService { | |||
|     private TransactionRepository transactionRepository; | ||||
| 
 | ||||
|     public UserTransactionsDto getUserTransactionsDto(String authToken, Integer limit, Integer offset) { | ||||
|         Optional<UserEntity> user = this.userService.getCurrentUser(authToken); | ||||
|         Optional<UserEntity> user = this.userService.getCurrentUser(); | ||||
|         if (user.isPresent()) { | ||||
|             List<TransactionEntity> transactionEntities = this.transactionRepository.findByUserIdWithLimit(user.get(), limit, offset); | ||||
|             Boolean hasMore = this.transactionRepository.hasMore(user.get(), limit, offset); | ||||
|  |  | |||
|  | @ -8,33 +8,15 @@ stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b3 | |||
| app.frontend-host=${FE_URL:http://localhost:4200} | ||||
| 
 | ||||
| spring.application.name=casino | ||||
| #client registration configuration | ||||
| 
 | ||||
| spring.security.oauth2.client.registration.authentik.client-id=${AUTH_CLIENT_ID:MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm} | ||||
| spring.security.oauth2.client.registration.authentik.client-secret=${AUTH_CLIENT_SECRET:GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5} | ||||
| spring.security.oauth2.client.registration.authentik.provider=authentik | ||||
| spring.security.oauth2.client.registration.authentik.client-name=Authentik | ||||
| spring.security.oauth2.client.registration.authentik.scope=openid,email,profile | ||||
| spring.security.oauth2.client.registration.authentik.client-authentication-method=client_secret_basic | ||||
| spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code | ||||
| spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} | ||||
| # JWT Configuration | ||||
| jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437} | ||||
| jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000} | ||||
| 
 | ||||
| # Provider settings | ||||
| spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTH_PROVIDER_ISSUER:https://oauth.simonis.lol/application/o/casino-dev/} | ||||
| spring.security.oauth2.client.provider.authentik.authorization-uri=${AUTH_PROVIDER_AUTHORIZE_URI:https://oauth.simonis.lol/application/o/authorize/} | ||||
| spring.security.oauth2.client.provider.authentik.token-uri=${AUTH_PROVIDER_TOKEN_URI:https://oauth.simonis.lol/application/o/token/} | ||||
| spring.security.oauth2.client.provider.authentik.user-info-uri=${AUTH_PROVIDER_USERINFO_URI:https://oauth.simonis.lol/application/o/userinfo/} | ||||
| spring.security.oauth2.client.provider.authentik.jwk-set-uri=${AUTH_PROVIDER_JWKS_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/} | ||||
| spring.security.oauth2.client.provider.authentik.user-name-attribute=${AUTH_PROVIDER_NAME_ATTR:preferred_username} | ||||
| 
 | ||||
| # Resource server config | ||||
| spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTH_JWT_ISSUER_URI:https://oauth.simonis.lol/application/o/casino-dev}/ | ||||
| spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${AUTH_JWT_JWT_SET_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/} | ||||
| 
 | ||||
| #OIDC provider configuration: | ||||
| # Logging | ||||
| logging.level.org.springframework.security=DEBUG | ||||
| #validating JWT token against our Authentik server | ||||
| 
 | ||||
| # Swagger  | ||||
| springdoc.swagger-ui.path=swagger | ||||
| springdoc.swagger-ui.try-it-out-enabled=true | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,8 +8,12 @@ export const routes: Routes = [ | |||
|     component: LandingComponent, | ||||
|   }, | ||||
|   { | ||||
|     path: 'auth/callback', | ||||
|     loadComponent: () => import('./feature/login-success/login-success.component'), | ||||
|     path: 'login', | ||||
|     loadComponent: () => import('./feature/auth/login/login.component').then(m => m.LoginComponent), | ||||
|   }, | ||||
|   { | ||||
|     path: 'register', | ||||
|     loadComponent: () => import('./feature/auth/register/register.component').then(m => m.RegisterComponent), | ||||
|   }, | ||||
|   { | ||||
|     path: 'home', | ||||
|  | @ -24,7 +28,6 @@ export const routes: Routes = [ | |||
|   { | ||||
|     path: 'game/slots', | ||||
|     loadComponent: () => import('./feature/game/slots/slots.component'), | ||||
|     canActivate: [authGuard], | ||||
|   }, | ||||
|   { | ||||
|     path: 'game/lootboxes', | ||||
|  |  | |||
							
								
								
									
										113
									
								
								frontend/src/app/feature/auth/login/login.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								frontend/src/app/feature/auth/login/login.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| import { Component } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; | ||||
| import { Router, RouterLink } from '@angular/router'; | ||||
| import { LoginRequest } from '../../../model/auth/LoginRequest'; | ||||
| import { AuthService } from '../../../service/auth.service'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-login', | ||||
|   standalone: true, | ||||
|   imports: [CommonModule, ReactiveFormsModule, RouterLink], | ||||
|   template: ` | ||||
|     <div class="min-h-screen bg-gray-900 flex items-center justify-center"> | ||||
|       <div class="max-w-md w-full bg-gray-800 rounded-lg shadow-lg p-8"> | ||||
|         <h2 class="text-2xl font-bold text-white mb-6 text-center">Login to Casino</h2> | ||||
| 
 | ||||
|         <div *ngIf="errorMessage" class="bg-red-600 text-white p-4 rounded mb-4"> | ||||
|           {{ errorMessage }} | ||||
|         </div> | ||||
| 
 | ||||
|         <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="space-y-6"> | ||||
|           <div> | ||||
|             <label for="usernameOrEmail" class="block text-sm font-medium text-gray-300">Username or Email</label> | ||||
|             <input | ||||
|               id="usernameOrEmail" | ||||
|               type="text" | ||||
|               formControlName="usernameOrEmail" | ||||
|               class="mt-1 block w-full bg-gray-700 border-gray-600 text-white rounded-md shadow-sm py-2 px-3" | ||||
|               placeholder="Enter your username or email"> | ||||
| 
 | ||||
|             <div *ngIf="form['usernameOrEmail'].touched && form['usernameOrEmail'].errors" class="text-red-500 mt-1 text-sm"> | ||||
|               <span *ngIf="form['usernameOrEmail'].errors?.['required']">Username or email is required</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <label for="password" class="block text-sm font-medium text-gray-300">Password</label> | ||||
|             <input | ||||
|               id="password" | ||||
|               type="password" | ||||
|               formControlName="password" | ||||
|               class="mt-1 block w-full bg-gray-700 border-gray-600 text-white rounded-md shadow-sm py-2 px-3" | ||||
|               placeholder="Enter your password"> | ||||
| 
 | ||||
|             <div *ngIf="form['password'].touched && form['password'].errors" class="text-red-500 mt-1 text-sm"> | ||||
|               <span *ngIf="form['password'].errors?.['required']">Password is required</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <button | ||||
|               type="submit" | ||||
|               [disabled]="loginForm.invalid || isLoading" | ||||
|               class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | ||||
|               {{ isLoading ? 'Logging in...' : 'Login' }} | ||||
|             </button> | ||||
|           </div> | ||||
|         </form> | ||||
| 
 | ||||
|         <div class="mt-6 text-center"> | ||||
|           <p class="text-sm text-gray-400"> | ||||
|             Don't have an account? | ||||
|             <a routerLink="/register" class="font-medium text-indigo-400 hover:text-indigo-300">Register</a> | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ` | ||||
| }) | ||||
| export class LoginComponent { | ||||
|   loginForm: FormGroup; | ||||
|   errorMessage = ''; | ||||
|   isLoading = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private fb: FormBuilder, | ||||
|     private authService: AuthService, | ||||
|     private router: Router | ||||
|   ) { | ||||
|     this.loginForm = this.fb.group({ | ||||
|       usernameOrEmail: ['', [Validators.required]], | ||||
|       password: ['', [Validators.required]] | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get form() { | ||||
|     return this.loginForm.controls; | ||||
|   } | ||||
| 
 | ||||
|   onSubmit(): void { | ||||
|     if (this.loginForm.invalid) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.isLoading = true; | ||||
|     this.errorMessage = ''; | ||||
| 
 | ||||
|     const loginRequest: LoginRequest = { | ||||
|       usernameOrEmail: this.form['usernameOrEmail'].value, | ||||
|       password: this.form['password'].value | ||||
|     }; | ||||
| 
 | ||||
|     this.authService.login(loginRequest).subscribe({ | ||||
|       next: () => { | ||||
|         this.router.navigate(['/home']); | ||||
|       }, | ||||
|       error: err => { | ||||
|         this.isLoading = false; | ||||
|         this.errorMessage = err.error?.message || 'Failed to login. Please check your credentials.'; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										144
									
								
								frontend/src/app/feature/auth/register/register.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								frontend/src/app/feature/auth/register/register.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,144 @@ | |||
| import { Component } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; | ||||
| import { Router, RouterLink } from '@angular/router'; | ||||
| import { RegisterRequest } from '../../../model/auth/RegisterRequest'; | ||||
| import { AuthService } from '../../../service/auth.service'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-register', | ||||
|   standalone: true, | ||||
|   imports: [CommonModule, ReactiveFormsModule, RouterLink], | ||||
|   template: ` | ||||
|     <div class="min-h-screen bg-gray-900 flex items-center justify-center"> | ||||
|       <div class="max-w-md w-full bg-gray-800 rounded-lg shadow-lg p-8"> | ||||
|         <h2 class="text-2xl font-bold text-white mb-6 text-center">Create Account</h2> | ||||
| 
 | ||||
|         <div *ngIf="errorMessage" class="bg-red-600 text-white p-4 rounded mb-4"> | ||||
|           {{ errorMessage }} | ||||
|         </div> | ||||
| 
 | ||||
|         <form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="space-y-6"> | ||||
|           <div> | ||||
|             <label for="email" class="block text-sm font-medium text-gray-300">Email</label> | ||||
|             <input | ||||
|               id="email" | ||||
|               type="email" | ||||
|               formControlName="email" | ||||
|               class="mt-1 block w-full bg-gray-700 border-gray-600 text-white rounded-md shadow-sm py-2 px-3" | ||||
|               placeholder="Enter your email"> | ||||
| 
 | ||||
|             <div *ngIf="form['email'].touched && form['email'].errors" class="text-red-500 mt-1 text-sm"> | ||||
|               <span *ngIf="form['email'].errors?.['required']">Email is required</span> | ||||
|               <span *ngIf="form['email'].errors?.['email']">Please enter a valid email address</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <label for="username" class="block text-sm font-medium text-gray-300">Username</label> | ||||
|             <input | ||||
|               id="username" | ||||
|               type="text" | ||||
|               formControlName="username" | ||||
|               class="mt-1 block w-full bg-gray-700 border-gray-600 text-white rounded-md shadow-sm py-2 px-3" | ||||
|               placeholder="Choose a username"> | ||||
| 
 | ||||
|             <div *ngIf="form['username'].touched && form['username'].errors" class="text-red-500 mt-1 text-sm"> | ||||
|               <span *ngIf="form['username'].errors?.['required']">Username is required</span> | ||||
|               <span *ngIf="form['username'].errors?.['minlength']">Username must be at least 3 characters</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <label for="password" class="block text-sm font-medium text-gray-300">Password</label> | ||||
|             <input | ||||
|               id="password" | ||||
|               type="password" | ||||
|               formControlName="password" | ||||
|               class="mt-1 block w-full bg-gray-700 border-gray-600 text-white rounded-md shadow-sm py-2 px-3" | ||||
|               placeholder="Create a password"> | ||||
| 
 | ||||
|             <div *ngIf="form['password'].touched && form['password'].errors" class="text-red-500 mt-1 text-sm"> | ||||
|               <span *ngIf="form['password'].errors?.['required']">Password is required</span> | ||||
|               <span *ngIf="form['password'].errors?.['minlength']">Password must be at least 6 characters</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <button | ||||
|               type="submit" | ||||
|               [disabled]="registerForm.invalid || isLoading" | ||||
|               class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | ||||
|               {{ isLoading ? 'Creating account...' : 'Register' }} | ||||
|             </button> | ||||
|           </div> | ||||
|         </form> | ||||
| 
 | ||||
|         <div class="mt-6 text-center"> | ||||
|           <p class="text-sm text-gray-400"> | ||||
|             Already have an account? | ||||
|             <a routerLink="/login" class="font-medium text-indigo-400 hover:text-indigo-300">Login</a> | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ` | ||||
| }) | ||||
| export class RegisterComponent { | ||||
|   registerForm: FormGroup; | ||||
|   errorMessage = ''; | ||||
|   isLoading = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private fb: FormBuilder, | ||||
|     private authService: AuthService, | ||||
|     private router: Router | ||||
|   ) { | ||||
|     this.registerForm = this.fb.group({ | ||||
|       email: ['', [Validators.required, Validators.email]], | ||||
|       username: ['', [Validators.required, Validators.minLength(3)]], | ||||
|       password: ['', [Validators.required, Validators.minLength(6)]] | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get form() { | ||||
|     return this.registerForm.controls; | ||||
|   } | ||||
| 
 | ||||
|   onSubmit(): void { | ||||
|     if (this.registerForm.invalid) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.isLoading = true; | ||||
|     this.errorMessage = ''; | ||||
| 
 | ||||
|     const registerRequest: RegisterRequest = { | ||||
|       email: this.form['email'].value, | ||||
|       username: this.form['username'].value, | ||||
|       password: this.form['password'].value | ||||
|     }; | ||||
| 
 | ||||
|     this.authService.register(registerRequest).subscribe({ | ||||
|       next: () => { | ||||
|         // After registration, log in the user
 | ||||
|         this.authService.login({ | ||||
|           usernameOrEmail: registerRequest.email, | ||||
|           password: registerRequest.password | ||||
|         }).subscribe({ | ||||
|           next: () => { | ||||
|             this.router.navigate(['/home']); | ||||
|           }, | ||||
|           error: err => { | ||||
|             this.isLoading = false; | ||||
|             this.errorMessage = 'Registration successful but failed to login automatically. Please log in manually.'; | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|       error: err => { | ||||
|         this.isLoading = false; | ||||
|         this.errorMessage = err.error?.message || 'Failed to register. Please try again.'; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -51,7 +51,7 @@ export default class BlackjackComponent implements OnInit { | |||
|   debtAmount = signal(0); | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.userService.currentUser$.subscribe((user) => { | ||||
|     this.userService.getCurrentUser().subscribe((user) => { | ||||
|       if (user) { | ||||
|         this.balance.set(user.balance); | ||||
|       } | ||||
|  | @ -83,7 +83,7 @@ export default class BlackjackComponent implements OnInit { | |||
| 
 | ||||
|     if (isGameOver) { | ||||
|       console.log('Game is over, state:', game.state); | ||||
|       this.userService.refreshCurrentUser(); | ||||
|       // this.userService.refreshCurrentUser();
 | ||||
|       timer(1500).subscribe(() => { | ||||
|         this.showGameResult.set(true); | ||||
|         console.log('Game result dialog shown after delay'); | ||||
|  | @ -97,7 +97,7 @@ export default class BlackjackComponent implements OnInit { | |||
|     this.blackjackService.startGame(bet).subscribe({ | ||||
|       next: (game) => { | ||||
|         this.updateGameState(game); | ||||
|         this.userService.refreshCurrentUser(); | ||||
|         // this.userService.refreshCurrentUser();
 | ||||
|         this.isActionInProgress.set(false); | ||||
|       }, | ||||
|       error: (error) => { | ||||
|  | @ -116,7 +116,7 @@ export default class BlackjackComponent implements OnInit { | |||
|       next: (game) => { | ||||
|         this.updateGameState(game); | ||||
|         if (game.state !== 'IN_PROGRESS') { | ||||
|           this.userService.refreshCurrentUser(); | ||||
|           // this.userService.refreshCurrentUser();
 | ||||
|         } | ||||
|         this.isActionInProgress.set(false); | ||||
|       }, | ||||
|  | @ -141,7 +141,7 @@ export default class BlackjackComponent implements OnInit { | |||
|     this.blackjackService.stand(this.currentGameId()!).subscribe({ | ||||
|       next: (game) => { | ||||
|         this.updateGameState(game); | ||||
|         this.userService.refreshCurrentUser(); | ||||
|         // this.userService.refreshCurrentUser();
 | ||||
|         this.isActionInProgress.set(false); | ||||
|       }, | ||||
|       error: (error) => { | ||||
|  | @ -184,7 +184,7 @@ export default class BlackjackComponent implements OnInit { | |||
|   onCloseGameResult(): void { | ||||
|     console.log('Closing game result dialog'); | ||||
|     this.showGameResult.set(false); | ||||
|     this.userService.refreshCurrentUser(); | ||||
|     // this.userService.refreshCurrentUser();
 | ||||
|   } | ||||
| 
 | ||||
|   onCloseDebtDialog(): void { | ||||
|  | @ -195,11 +195,11 @@ export default class BlackjackComponent implements OnInit { | |||
|     if (error instanceof HttpErrorResponse) { | ||||
|       if (error.status === 400 && error.error?.error === 'Invalid state') { | ||||
|         this.gameInProgress.set(false); | ||||
|         this.userService.refreshCurrentUser(); | ||||
|         // this.userService.refreshCurrentUser();
 | ||||
|       } else if (error.status === 500) { | ||||
|         console.log('Server error occurred. The game may have been updated in another session.'); | ||||
|         this.gameInProgress.set(false); | ||||
|         this.userService.refreshCurrentUser(); | ||||
|         // this.userService.refreshCurrentUser();
 | ||||
|         if (this.currentGameId()) { | ||||
|           this.refreshGameState(this.currentGameId()!); | ||||
|         } | ||||
|  |  | |||
|  | @ -10,9 +10,14 @@ | |||
|         <div class="welcome-bonus">200% bis zu 500€</div> | ||||
|         <p class="bonus-description">+ 200 Freispiele</p> | ||||
| 
 | ||||
|         <button class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"> | ||||
|           Bonus Sichern | ||||
|         </button> | ||||
|         <div class="flex justify-center space-x-4 mt-6"> | ||||
|           <a routerLink="/register" class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"> | ||||
|             Konto erstellen | ||||
|           </a> | ||||
|           <a routerLink="/login" class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded"> | ||||
|             Anmelden | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="relative mb-16"> | ||||
|  |  | |||
|  | @ -1,11 +1,12 @@ | |||
| import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { NgFor } from '@angular/common'; | ||||
| import { NavbarComponent } from '@shared/components/navbar/navbar.component'; | ||||
| import { RouterLink } from '@angular/router'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-landing-page', | ||||
|   standalone: true, | ||||
|   imports: [NavbarComponent, NgFor], | ||||
|   imports: [NavbarComponent, NgFor, RouterLink], | ||||
|   templateUrl: './landing.component.html', | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| export interface User { | ||||
|   authentikId: string; | ||||
|   id: number; | ||||
|   email: string; | ||||
|   username: string; | ||||
|   balance: number; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										4
									
								
								frontend/src/app/model/auth/AuthResponse.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/app/model/auth/AuthResponse.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| export interface AuthResponse { | ||||
|   token: string; | ||||
|   tokenType: string; | ||||
| } | ||||
							
								
								
									
										4
									
								
								frontend/src/app/model/auth/LoginRequest.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/app/model/auth/LoginRequest.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| export interface LoginRequest { | ||||
|   usernameOrEmail: string; | ||||
|   password: string; | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/app/model/auth/RegisterRequest.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/app/model/auth/RegisterRequest.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export interface RegisterRequest { | ||||
|   email: string; | ||||
|   username: string; | ||||
|   password: string; | ||||
| } | ||||
|  | @ -1,208 +1,96 @@ | |||
| import { inject, Injectable } from '@angular/core'; | ||||
| import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; | ||||
| import { UserService } from './user.service'; | ||||
| import { User } from '../model/User'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { BehaviorSubject, Observable, tap } from 'rxjs'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { LoginRequest } from '../model/auth/LoginRequest'; | ||||
| import { RegisterRequest } from '../model/auth/RegisterRequest'; | ||||
| import { AuthResponse } from '../model/auth/AuthResponse'; | ||||
| import { User } from '../model/User'; | ||||
| import { environment } from '../../environments/environment'; | ||||
| import { catchError, from, of } from 'rxjs'; | ||||
| 
 | ||||
| const TOKEN_KEY = 'auth-token'; | ||||
| const USER_KEY = 'auth-user'; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class AuthService { | ||||
|   private readonly authConfig: AuthConfig = { | ||||
|     issuer: 'https://oauth.simonis.lol/application/o/casino-dev/', | ||||
|     clientId: environment.OAUTH_CLIENT_ID, | ||||
|     dummyClientSecret: environment.OAUTH_CLIENT_SECRET, | ||||
|     scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`, | ||||
|     responseType: 'code', | ||||
|     redirectUri: window.location.origin + '/auth/callback', | ||||
|     postLogoutRedirectUri: '', | ||||
|     redirectUriAsPostLogoutRedirectUriFallback: false, | ||||
|     oidc: true, | ||||
|     requestAccessToken: true, | ||||
|     tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/', | ||||
|     userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/', | ||||
|     strictDiscoveryDocumentValidation: false, | ||||
|     skipIssuerCheck: true, | ||||
|     disableAtHashCheck: true, | ||||
|     requireHttps: false, | ||||
|     showDebugInformation: false, | ||||
|     sessionChecksEnabled: false, | ||||
|   }; | ||||
| 
 | ||||
|   private userService: UserService = inject(UserService); | ||||
|   private oauthService: OAuthService = inject(OAuthService); | ||||
|   private router: Router = inject(Router); | ||||
| 
 | ||||
|   private user: User | null = null; | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.oauthService.configure(this.authConfig); | ||||
|     this.setupEventHandling(); | ||||
| 
 | ||||
|     const hasAuthParams = | ||||
|       window.location.search.includes('code=') || | ||||
|       window.location.search.includes('token=') || | ||||
|       window.location.search.includes('id_token='); | ||||
| 
 | ||||
|     if (hasAuthParams) { | ||||
|       this.processCodeFlow(); | ||||
|     } else { | ||||
|       this.checkExistingSession(); | ||||
|   private authUrl = `${environment.apiUrl}/api/auth`; | ||||
|   private userUrl = `${environment.apiUrl}/api/users`; | ||||
|    | ||||
|   private currentUserSubject: BehaviorSubject<User | null>; | ||||
|   public currentUser: Observable<User | null>; | ||||
|    | ||||
|   constructor(private http: HttpClient, private router: Router) { | ||||
|     this.currentUserSubject = new BehaviorSubject<User | null>(this.getUserFromStorage()); | ||||
|     this.currentUser = this.currentUserSubject.asObservable(); | ||||
|      | ||||
|     // Check if token exists and load user data
 | ||||
|     if (this.getToken()) { | ||||
|       this.loadCurrentUser(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private processCodeFlow() { | ||||
|     this.oauthService | ||||
|       .tryLogin({ | ||||
|         onTokenReceived: () => { | ||||
|           this.handleSuccessfulLogin(); | ||||
|         }, | ||||
|       }) | ||||
|       .catch((err) => { | ||||
|         console.error('Error processing code flow:', err); | ||||
|       }); | ||||
|    | ||||
|   public get currentUserValue(): User | null { | ||||
|     return this.currentUserSubject.value; | ||||
|   } | ||||
| 
 | ||||
|   private checkExistingSession() { | ||||
|     this.oauthService | ||||
|       .loadDiscoveryDocumentAndTryLogin() | ||||
|       .then((isLoggedIn) => { | ||||
|         if (isLoggedIn && !this.user) { | ||||
|           this.handleSuccessfulLogin(); | ||||
|         } | ||||
|       }) | ||||
|       .catch((err) => { | ||||
|         console.error('Error during initial login attempt:', err); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   private setupEventHandling() { | ||||
|     this.oauthService.events.subscribe((event: OAuthEvent) => { | ||||
|       if (event.type === 'token_received') { | ||||
|         this.handleSuccessfulLogin(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private handleSuccessfulLogin() { | ||||
|     const claims = this.oauthService.getIdentityClaims(); | ||||
| 
 | ||||
|     if (claims && (claims['sub'] || claims['email'])) { | ||||
|       this.processUserProfile(claims); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       from(this.oauthService.loadUserProfile()) | ||||
|         .pipe( | ||||
|           catchError((error) => { | ||||
|             console.error('Error loading user profile:', error); | ||||
|             if (this.oauthService.hasValidAccessToken()) { | ||||
|               this.oauthService.getAccessToken(); | ||||
|               const minimalProfile = { | ||||
|                 sub: 'user-' + Math.random().toString(36).substring(2, 10), | ||||
|                 preferred_username: 'user' + Date.now(), | ||||
|               }; | ||||
|               return of({ info: minimalProfile }); | ||||
|             } | ||||
|             return of(null); | ||||
|           }) | ||||
|         ) | ||||
|         .subscribe((profile) => { | ||||
|           if (profile) { | ||||
|             this.processUserProfile(profile); | ||||
|           } else { | ||||
|             this.router.navigate(['/']); | ||||
|           } | ||||
|         }); | ||||
|     } catch (err) { | ||||
|       console.error('Exception in handleSuccessfulLogin:', err); | ||||
|       if (this.oauthService.hasValidAccessToken()) { | ||||
|         this.router.navigate(['/home']); | ||||
|       } else { | ||||
|         this.router.navigate(['/']); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private processUserProfile(profile: unknown) { | ||||
|     this.fromUserProfile(profile as Record<string, unknown>).subscribe({ | ||||
|       next: (user) => { | ||||
|         this.user = user; | ||||
|         this.router.navigate(['home']); | ||||
|       }, | ||||
|       error: (err) => { | ||||
|         console.error('Error creating/retrieving user:', err); | ||||
|         if (this.oauthService.hasValidAccessToken()) { | ||||
|           this.router.navigate(['/home']); | ||||
|         } else { | ||||
|           this.router.navigate(['/']); | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   login() { | ||||
|     try { | ||||
|       this.oauthService | ||||
|         .loadDiscoveryDocument() | ||||
|         .then(() => { | ||||
|           this.oauthService.initLoginFlow(); | ||||
|    | ||||
|   login(loginRequest: LoginRequest): Observable<AuthResponse> { | ||||
|     return this.http.post<AuthResponse>(`${this.authUrl}/login`, loginRequest) | ||||
|       .pipe( | ||||
|         tap(response => { | ||||
|           this.setToken(response.token); | ||||
|           this.loadCurrentUser(); | ||||
|         }) | ||||
|         .catch((err) => { | ||||
|           console.error('Error loading discovery document:', err); | ||||
|           this.oauthService.initLoginFlow(); | ||||
|         }); | ||||
|     } catch (err) { | ||||
|       console.error('Exception in login:', err); | ||||
|       const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback'; | ||||
|       const scope = this.authConfig.scope || 'openid email profile'; | ||||
|       const authUrl = `${this.authConfig.issuer}authorize?client_id=${this.authConfig.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`; | ||||
|       window.location.href = authUrl; | ||||
|     } | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   logout() { | ||||
|     try { | ||||
|       this.user = null; | ||||
| 
 | ||||
|       this.oauthService.logOut(true); | ||||
| 
 | ||||
|       if (window.location.href.includes('id_token') || window.location.href.includes('logout')) { | ||||
|         window.location.href = window.location.origin; | ||||
|       } | ||||
| 
 | ||||
|       localStorage.removeItem('access_token'); | ||||
|       localStorage.removeItem('id_token'); | ||||
|       localStorage.removeItem('refresh_token'); | ||||
|       sessionStorage.removeItem('access_token'); | ||||
|       sessionStorage.removeItem('id_token'); | ||||
|       sessionStorage.removeItem('refresh_token'); | ||||
| 
 | ||||
|       this.router.navigate(['/']); | ||||
|     } catch (err) { | ||||
|       console.error('Exception in logout:', err); | ||||
|       localStorage.clear(); | ||||
|       sessionStorage.clear(); | ||||
|       this.router.navigate(['/']); | ||||
|     } | ||||
|    | ||||
|   register(registerRequest: RegisterRequest): Observable<User> { | ||||
|     return this.http.post<User>(`${this.authUrl}/register`, registerRequest); | ||||
|   } | ||||
| 
 | ||||
|   isLoggedIn() { | ||||
|     return this.oauthService.hasValidAccessToken(); | ||||
|    | ||||
|   logout(): void { | ||||
|     localStorage.removeItem(TOKEN_KEY); | ||||
|     localStorage.removeItem(USER_KEY); | ||||
|     this.currentUserSubject.next(null); | ||||
|     this.router.navigate(['/']); | ||||
|   } | ||||
| 
 | ||||
|   private fromUserProfile(profile: Record<string, unknown>) { | ||||
|     return this.userService.getOrCreateUser(profile); | ||||
|    | ||||
|   isLoggedIn(): boolean { | ||||
|     return !!this.getToken(); | ||||
|   } | ||||
| 
 | ||||
|   getAccessToken() { | ||||
|     return this.oauthService.getAccessToken(); | ||||
|    | ||||
|   getToken(): string | null { | ||||
|     return localStorage.getItem(TOKEN_KEY); | ||||
|   } | ||||
| 
 | ||||
|   getUser() { | ||||
|     return this.user; | ||||
|    | ||||
|   private setToken(token: string): void { | ||||
|     localStorage.setItem(TOKEN_KEY, token); | ||||
|   } | ||||
|    | ||||
|   private setUser(user: User): void { | ||||
|     localStorage.setItem(USER_KEY, JSON.stringify(user)); | ||||
|     this.currentUserSubject.next(user); | ||||
|   } | ||||
|    | ||||
|   private getUserFromStorage(): User | null { | ||||
|     const user = localStorage.getItem(USER_KEY); | ||||
|     return user ? JSON.parse(user) : null; | ||||
|   } | ||||
|    | ||||
|   private loadCurrentUser(): void { | ||||
|     this.http.get<User>(`${this.userUrl}/me`) | ||||
|       .subscribe({ | ||||
|         next: (user) => { | ||||
|           this.setUser(user); | ||||
|         }, | ||||
|         error: () => { | ||||
|           this.logout(); | ||||
|         } | ||||
|       }); | ||||
|   } | ||||
|    | ||||
|   getUser(): User | null { | ||||
|     return this.currentUserValue; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,25 +1,24 @@ | |||
| import { inject, Injectable } from '@angular/core'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs'; | ||||
| import { User } from '../model/User'; | ||||
| import { environment } from '@environments/environment'; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class UserService { | ||||
|   private http: HttpClient = inject(HttpClient); | ||||
|   private apiUrl = `${environment.apiUrl}/api/users`; | ||||
|   private currentUserSubject = new BehaviorSubject<User | null>(null); | ||||
|   public currentUser$ = this.currentUserSubject.asObservable(); | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.getCurrentUser().subscribe(); | ||||
|   constructor(private http: HttpClient) {} | ||||
| 
 | ||||
|   public getUserById(id: number): Observable<User> { | ||||
|     return this.http.get<User>(`${this.apiUrl}/${id}`); | ||||
|   } | ||||
| 
 | ||||
|   public getUser(id: string): Observable<User | null> { | ||||
|     return this.http.get<User | null>(`/backend/user/${id}`).pipe( | ||||
|       catchError(() => EMPTY), | ||||
|       tap((user) => this.currentUserSubject.next(user)) | ||||
|     ); | ||||
|   public getUserByUsername(username: string): Observable<User> { | ||||
|     return this.http.get<User>(`${this.apiUrl}/username/${username}`); | ||||
|   } | ||||
| 
 | ||||
|   public getCurrentUser(): Observable<User | null> { | ||||
|  | @ -52,20 +51,4 @@ export class UserService { | |||
|       }) | ||||
|       .pipe(tap((user) => this.currentUserSubject.next(user))); | ||||
|   } | ||||
| 
 | ||||
|   public getOrCreateUser(profile: Record<string, unknown>): Observable<User> { | ||||
|     const info = profile['info'] as Record<string, unknown> | undefined; | ||||
|     const id = (info?.['sub'] as string) || (profile['sub'] as string); | ||||
|     const username = | ||||
|       (info?.['preferred_username'] as string) || | ||||
|       (profile['preferred_username'] as string) || | ||||
|       (profile['email'] as string) || | ||||
|       (profile['name'] as string); | ||||
| 
 | ||||
|     if (!id || !username) { | ||||
|       throw new Error('Invalid user profile data'); | ||||
|     } | ||||
| 
 | ||||
|     return this.createUser(id, username); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -12,7 +12,8 @@ | |||
| 
 | ||||
|       <div class="hidden md:flex items-center space-x-4"> | ||||
|         @if (!isLoggedIn) { | ||||
|           <button (click)="login()" class="button-primary px-4 py-1.5">Anmelden</button> | ||||
|           <a routerLink="/login" class="button-primary px-4 py-1.5">Anmelden</a> | ||||
|           <a routerLink="/register" class="bg-emerald-700 text-white hover:bg-emerald-600 px-4 py-1.5 rounded">Registrieren</a> | ||||
|         } | ||||
|         @if (isLoggedIn) { | ||||
|           <div | ||||
|  | @ -66,7 +67,8 @@ | |||
|         <a routerLink="/games" class="nav-mobile-link">Spiele</a> | ||||
|         <div class="pt-2 space-y-2"> | ||||
|           @if (!isLoggedIn) { | ||||
|             <button (click)="login()" class="button-primary w-full py-1.5">Anmelden</button> | ||||
|             <a routerLink="/login" class="button-primary w-full py-1.5 block text-center">Anmelden</a> | ||||
|             <a routerLink="/register" class="bg-emerald-700 text-white hover:bg-emerald-600 w-full py-1.5 rounded block text-center">Registrieren</a> | ||||
|           } | ||||
|           @if (isLoggedIn) { | ||||
|             <button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button> | ||||
|  |  | |||
|  | @ -26,10 +26,13 @@ export class NavbarComponent implements OnInit, OnDestroy { | |||
| 
 | ||||
|   private userService = inject(UserService); | ||||
|   private userSubscription: Subscription | undefined; | ||||
|   private authSubscription: Subscription | undefined; | ||||
|   public balance = signal(0); | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.userSubscription = this.userService.currentUser$.subscribe((user) => { | ||||
|     // Subscribe to auth changes
 | ||||
|     this.authSubscription = this.authService.currentUser.subscribe(user => { | ||||
|       this.isLoggedIn = !!user; | ||||
|       this.balance.set(user?.balance ?? 0); | ||||
|       console.log('Updated navbar balance:', user?.balance); | ||||
|     }); | ||||
|  | @ -41,13 +44,8 @@ export class NavbarComponent implements OnInit, OnDestroy { | |||
|     if (this.userSubscription) { | ||||
|       this.userSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   login() { | ||||
|     try { | ||||
|       this.authService.login(); | ||||
|     } catch (error) { | ||||
|       console.error('Login failed:', error); | ||||
|     if (this.authSubscription) { | ||||
|       this.authSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,21 +1,33 @@ | |||
| import { HttpInterceptorFn } from '@angular/common/http'; | ||||
| import { inject } from '@angular/core'; | ||||
| import { OAuthStorage } from 'angular-oauth2-oidc'; | ||||
| import { AuthService } from '../../service/auth.service'; | ||||
| 
 | ||||
| export const httpInterceptor: HttpInterceptorFn = (req, next) => { | ||||
|   const oauthStorage = inject(OAuthStorage); | ||||
|   const authService = inject(AuthService); | ||||
|   const token = authService.getToken(); | ||||
| 
 | ||||
|   if (oauthStorage.getItem('access_token')) { | ||||
|   // Always add CORS headers
 | ||||
|   if (token) { | ||||
|     return next( | ||||
|       req.clone({ | ||||
|         setHeaders: { | ||||
|           Authorization: 'Bearer ' + oauthStorage.getItem('access_token'), | ||||
|           'Access-Control-Allow-Origin': '*', | ||||
|           Authorization: `Bearer ${token}`, | ||||
|           'Referrer-Policy': 'no-referrer', | ||||
|           'Access-Control-Allow-Origin': '*', | ||||
|           'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||||
|           'Access-Control-Allow-Headers': '*' | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|   } else { | ||||
|     return next(req); | ||||
|     return next( | ||||
|       req.clone({ | ||||
|         setHeaders: { | ||||
|           'Access-Control-Allow-Origin': '*', | ||||
|           'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||||
|           'Access-Control-Allow-Headers': '*' | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| export const environment = { | ||||
|   STRIPE_KEY: | ||||
|     'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG', | ||||
|   OAUTH_CLIENT_ID: 'MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm', | ||||
|   OAUTH_CLIENT_SECRET: | ||||
|     'GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5', | ||||
|   apiUrl: 'http://localhost:8080', | ||||
| }; | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| { | ||||
|   "/backend": { | ||||
|   "/api": { | ||||
|     "target": "http://localhost:8080/", | ||||
|     "secure": false, | ||||
|     "logLevel": "debug", | ||||
|     "pathRewrite": { | ||||
|       "^/backend": "" | ||||
|       "^/api": "" | ||||
|     }, | ||||
|     "changeOrigin": true | ||||
|   } | ||||
|  |  | |||
		Reference in a new issue