diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index a0b6899..51e3cd6 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -51,9 +51,6 @@ 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 { diff --git a/backend/src/main/java/de/szut/casino/CasinoApplication.java b/backend/src/main/java/de/szut/casino/CasinoApplication.java index 2818f23..1d7e861 100644 --- a/backend/src/main/java/de/szut/casino/CasinoApplication.java +++ b/backend/src/main/java/de/szut/casino/CasinoApplication.java @@ -13,6 +13,7 @@ import org.springframework.web.client.RestTemplate; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; @SpringBootApplication public class CasinoApplication { diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java index d4c0e6e..6813236 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java @@ -11,8 +11,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.Objects; -import java.util.Optional; +import java.math.BigDecimal; +import java.util.*; @Slf4j @RestController @@ -29,8 +29,8 @@ public class BlackJackGameController { } @GetMapping("/blackjack/{id}") - public ResponseEntity getGame(@PathVariable Long id) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity getGame(@PathVariable Long id, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); @@ -46,8 +46,8 @@ public class BlackJackGameController { } @PostMapping("/blackjack/{id}/hit") - public ResponseEntity hit(@PathVariable Long id) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity hit(@PathVariable Long id, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); @@ -63,8 +63,8 @@ public class BlackJackGameController { } @PostMapping("/blackjack/{id}/stand") - public ResponseEntity stand(@PathVariable Long id) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity stand(@PathVariable Long id, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); @@ -80,8 +80,8 @@ public class BlackJackGameController { } @PostMapping("/blackjack/{id}/doubleDown") - public ResponseEntity doubleDown(@PathVariable Long id) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity doubleDown(@PathVariable Long id, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); @@ -97,8 +97,8 @@ public class BlackJackGameController { } @PostMapping("/blackjack/{id}/split") - public ResponseEntity split(@PathVariable Long id) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity split(@PathVariable Long id, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); @@ -114,8 +114,8 @@ public class BlackJackGameController { } @PostMapping("/blackjack/start") - public ResponseEntity createBlackJackGame(@RequestBody @Valid BetDto betDto) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity createBlackJackGame(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); diff --git a/backend/src/main/java/de/szut/casino/config/WebConfig.java b/backend/src/main/java/de/szut/casino/config/WebConfig.java deleted file mode 100644 index bb11293..0000000 --- a/backend/src/main/java/de/szut/casino/config/WebConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -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); - } - }; - } -} diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java index 522e47d..dbfe449 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -7,14 +7,19 @@ import com.stripe.param.checkout.SessionCreateParams; import de.szut.casino.deposit.dto.AmountDto; import de.szut.casino.deposit.dto.SessionIdDto; import de.szut.casino.user.UserEntity; -import de.szut.casino.user.UserService; +import de.szut.casino.user.UserRepository; +import de.szut.casino.user.dto.KeycloakUserDto; import jakarta.validation.Valid; 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.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; import java.util.Optional; @@ -29,18 +34,23 @@ public class DepositController { private final TransactionService transactionService; - private UserService userService; + private final RestTemplate restTemplate; - public DepositController(TransactionService transactionService, UserService userService) { + private final UserRepository userRepository; + + + public DepositController(TransactionService transactionService, RestTemplate restTemplate, UserRepository userRepository) { this.transactionService = transactionService; - this.userService = userService; + this.restTemplate = restTemplate; + this.userRepository = userRepository; } @PostMapping("/deposit/checkout") public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException { Stripe.apiKey = stripeKey; - Optional optionalUserEntity = this.userService.getCurrentUser(); + KeycloakUserDto userData = getAuthentikUserInfo(token); + Optional optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub()); SessionCreateParams params = SessionCreateParams.builder() .addLineItem(SessionCreateParams.LineItem.builder() @@ -68,5 +78,13 @@ public class DepositController { return ResponseEntity.ok(new SessionIdDto(session.getId())); } + + private KeycloakUserDto getAuthentikUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + + return response.getBody(); + } } diff --git a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/UserNotFoundException.java b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/UserNotFoundException.java index d843af7..6916a66 100644 --- a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/UserNotFoundException.java +++ b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/UserNotFoundException.java @@ -6,6 +6,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value = HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException() { - super("User not found"); + super("user not found"); } } diff --git a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java index 540e4c5..4ef8247 100644 --- a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java +++ b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java @@ -3,15 +3,13 @@ package de.szut.casino.lootboxes; import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException; import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; import de.szut.casino.user.UserService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; @RestController public class LootBoxController { @@ -19,7 +17,7 @@ public class LootBoxController { private final UserService userService; private final LootBoxService lootBoxService; - public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) { + public LootBoxController(LootBoxRepository lootBoxRepository, UserRepository userRepository, UserService userService, LootBoxService lootBoxService) { this.lootBoxRepository = lootBoxRepository; this.userService = userService; this.lootBoxService = lootBoxService; @@ -31,7 +29,7 @@ public class LootBoxController { } @PostMapping("/lootboxes/{id}") - public ResponseEntity purchaseLootBox(@PathVariable Long id) { + public ResponseEntity purchaseLootBox(@PathVariable Long id, @RequestHeader("Authorization") String token) { Optional optionalLootBox = lootBoxRepository.findById(id); if (optionalLootBox.isEmpty()) { return ResponseEntity.notFound().build(); @@ -39,7 +37,7 @@ public class LootBoxController { LootBoxEntity lootBox = optionalLootBox.get(); - Optional optionalUser = userService.getCurrentUser(); + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); } diff --git a/backend/src/main/java/de/szut/casino/security/AuthController.java b/backend/src/main/java/de/szut/casino/security/AuthController.java deleted file mode 100644 index b5c0639..0000000 --- a/backend/src/main/java/de/szut/casino/security/AuthController.java +++ /dev/null @@ -1,34 +0,0 @@ -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.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/auth") -public class AuthController { - - @Autowired - private AuthService authService; - - @PostMapping("/login") - public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) { - AuthResponseDto response = authService.login(loginRequest); - return ResponseEntity.ok(response); - } - - @PostMapping("/register") - public ResponseEntity registerUser(@Valid @RequestBody CreateUserDto signUpRequest) { - GetUserDto response = authService.register(signUpRequest); - return ResponseEntity.ok(response); - } -} diff --git a/backend/src/main/java/de/szut/casino/security/CorsFilter.java b/backend/src/main/java/de/szut/casino/security/CorsFilter.java deleted file mode 100644 index 032088f..0000000 --- a/backend/src/main/java/de/szut/casino/security/CorsFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -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); - } -} diff --git a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java index 155de8d..65d5b2c 100644 --- a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java +++ b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java @@ -1,22 +1,12 @@ 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.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.Customizer; 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; @@ -26,51 +16,23 @@ 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(cors -> cors.configurationSource(corsConfigurationSource())) + .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> { - auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error").permitAll() - .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() + auth.requestMatchers("/webhook", "/swagger/**", "/swagger-ui/**", "/health").permitAll() .anyRequest().authenticated(); }) - .authenticationProvider(authenticationProvider()) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> + jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter()) + )); return http.build(); } @@ -80,10 +42,9 @@ 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", "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.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Access-Control-Allow-Origin")); + configuration.setExposedHeaders(List.of("x-auth-token")); configuration.setAllowCredentials(true); - configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; diff --git a/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java b/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java deleted file mode 100644 index 0dd03dd..0000000 --- a/backend/src/main/java/de/szut/casino/security/dto/AuthResponseDto.java +++ /dev/null @@ -1,19 +0,0 @@ -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; - } -} diff --git a/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java b/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java deleted file mode 100644 index ace3508..0000000 --- a/backend/src/main/java/de/szut/casino/security/dto/LoginRequestDto.java +++ /dev/null @@ -1,19 +0,0 @@ -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; -} diff --git a/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java b/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java deleted file mode 100644 index 502ba0a..0000000 --- a/backend/src/main/java/de/szut/casino/security/jwt/JwtAuthenticationFilter.java +++ /dev/null @@ -1,64 +0,0 @@ -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; - } -} diff --git a/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java b/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java deleted file mode 100644 index 3c5ef86..0000000 --- a/backend/src/main/java/de/szut/casino/security/jwt/JwtUtils.java +++ /dev/null @@ -1,83 +0,0 @@ -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 claims = new HashMap<>(); - return createToken(claims, username); - } - - private String createToken(Map 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 extractClaim(String token, Function 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)); - } -} diff --git a/backend/src/main/java/de/szut/casino/security/service/AuthService.java b/backend/src/main/java/de/szut/casino/security/service/AuthService.java deleted file mode 100644 index 88687ba..0000000 --- a/backend/src/main/java/de/szut/casino/security/service/AuthService.java +++ /dev/null @@ -1,50 +0,0 @@ -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() - ); - } -} diff --git a/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java b/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java deleted file mode 100644 index b473a35..0000000 --- a/backend/src/main/java/de/szut/casino/security/service/UserDetailsServiceImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.szut.casino.security.service; - -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 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<>()); - } -} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotController.java b/backend/src/main/java/de/szut/casino/slots/SlotController.java index b55e2b7..5e27b52 100644 --- a/backend/src/main/java/de/szut/casino/slots/SlotController.java +++ b/backend/src/main/java/de/szut/casino/slots/SlotController.java @@ -8,12 +8,10 @@ import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -31,8 +29,8 @@ public class SlotController { } @PostMapping("/slots/spin") - public ResponseEntity spinSlots(@RequestBody @Valid BetDto betDto) { - Optional optionalUser = userService.getCurrentUser(); + public ResponseEntity spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { throw new UserNotFoundException(); diff --git a/backend/src/main/java/de/szut/casino/user/UserController.java b/backend/src/main/java/de/szut/casino/user/UserController.java index 6255a03..a5cae8c 100644 --- a/backend/src/main/java/de/szut/casino/user/UserController.java +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -1,28 +1,43 @@ 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.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController -@CrossOrigin -@RequestMapping("/users") public class UserController { @Autowired private UserService userService; - @Autowired - private UserMappingService userMappingService; + @PostMapping("/user") + public ResponseEntity createUser(@RequestBody @Valid CreateUserDto userData) { + if (userService.exists(userData.getAuthentikId())) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Location", "/user"); - @GetMapping("/me") - public ResponseEntity getCurrentUser() { - return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser().orElseThrow())); + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } + + return ResponseEntity.ok(userService.createUser(userData)); + } + + @GetMapping("/user") + public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { + GetUserDto userData = userService.getCurrentUserAsDto(token); + + if (userData == null) { + throw new UserNotFoundException(); + } + + return ResponseEntity.ok(userData); } } diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 913e69d..03f4a34 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -18,22 +18,16 @@ 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 email, String username, String password, BigDecimal balance) { - this.email = email; + public UserEntity(String authentikId, String username, BigDecimal balance) { + this.authentikId = authentikId; this.username = username; - this.password = password; this.balance = balance; } diff --git a/backend/src/main/java/de/szut/casino/user/UserMappingService.java b/backend/src/main/java/de/szut/casino/user/UserMappingService.java index e244646..86a1331 100644 --- a/backend/src/main/java/de/szut/casino/user/UserMappingService.java +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -1,13 +1,19 @@ package de.szut.casino.user; +import de.szut.casino.user.dto.CreateUserDto; import de.szut.casino.user.dto.GetUserDto; import org.springframework.stereotype.Service; +import java.math.BigDecimal; + @Service public class UserMappingService { - public GetUserDto mapToGetUserDto(UserEntity user) { - return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance()); + return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance()); + } + + public UserEntity mapToUserEntity(CreateUserDto createUserDto) { + return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO); } } diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java index 863e744..1f8d64e 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -1,17 +1,15 @@ 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 { - Optional findByUsername(String username); - - Optional findByEmail(String email); - - boolean existsByUsername(String username); - - boolean existsByEmail(String email); + @Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1") + Optional findOneByAuthentikId(String authentikId); + + boolean existsByAuthentikId(String authentikId); } diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index 66f2d52..d5ce222 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -1,12 +1,16 @@ package de.szut.casino.user; 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.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; -import java.math.BigDecimal; import java.util.Optional; @Service @@ -15,30 +19,63 @@ public class UserService { private UserRepository userRepository; @Autowired - private PasswordEncoder passwordEncoder; + private RestTemplate http; + + @Autowired + private UserMappingService mappingService; public UserEntity createUser(CreateUserDto createUserDto) { - if (userRepository.existsByUsername(createUserDto.getUsername())) { - throw new IllegalArgumentException("Username is already taken"); - } + UserEntity user = mappingService.mapToUserEntity(createUserDto); + userRepository.save(user); - if (userRepository.existsByEmail(createUserDto.getEmail())) { - throw new IllegalArgumentException("Email is already in use"); - } - - UserEntity user = new UserEntity( - createUserDto.getEmail(), - createUserDto.getUsername(), - passwordEncoder.encode(createUserDto.getPassword()), - BigDecimal.valueOf(10) // Starting balance - ); - - return userRepository.save(user); + return user; } - public Optional getCurrentUser() { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); + public GetUserDto getUser(String authentikId) { + Optional user = this.userRepository.findOneByAuthentikId(authentikId); - return userRepository.findByUsername(username); + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + } + + public GetUserDto getCurrentUserAsDto(String token) { + KeycloakUserDto userData = getAuthentikUserInfo(token); + + if (userData == null) { + return null; + } + Optional user = this.userRepository.findOneByAuthentikId(userData.getSub()); + + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + } + + public Optional getCurrentUser(String token) { + KeycloakUserDto userData = getAuthentikUserInfo(token); + + if (userData == null) { + return Optional.empty(); + } + return this.userRepository.findOneByAuthentikId(userData.getSub()); + } + + private KeycloakUserDto getAuthentikUserInfo(String token) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity 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 boolean exists(String authentikId) { + return userRepository.existsByAuthentikId(authentikId); } } diff --git a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java index f9969cc..f983b2d 100644 --- a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java +++ b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java @@ -1,8 +1,5 @@ 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; @@ -13,15 +10,6 @@ import lombok.Setter; @AllArgsConstructor @NoArgsConstructor public class CreateUserDto { - @NotBlank(message = "Email is required") - @Email(message = "Email should be valid") - private String email; - - @NotBlank(message = "Username is required") - @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") + private String authentikId; private String username; - - @NotBlank(message = "Password is required") - @Size(min = 6, message = "Password must be at least 6 characters") - private String password; } diff --git a/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java index 2c41f0d..7a7d561 100644 --- a/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java +++ b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java @@ -12,8 +12,7 @@ import java.math.BigDecimal; @AllArgsConstructor @NoArgsConstructor public class GetUserDto { - private Long id; - private String email; + private String authentikId; private String username; private BigDecimal balance; } diff --git a/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java new file mode 100644 index 0000000..4238e13 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java @@ -0,0 +1,15 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class KeycloakUserDto { + private String sub; + private String preferred_username; +} diff --git a/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java b/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java index 2a8350f..831c438 100644 --- a/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java +++ b/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java @@ -22,7 +22,7 @@ public class GetTransactionService { private TransactionRepository transactionRepository; public UserTransactionsDto getUserTransactionsDto(String authToken, Integer limit, Integer offset) { - Optional user = this.userService.getCurrentUser(); + Optional user = this.userService.getCurrentUser(authToken); if (user.isPresent()) { List transactionEntities = this.transactionRepository.findByUserIdWithLimit(user.get(), limit, offset); Boolean hasMore = this.transactionRepository.hasMore(user.get(), limit, offset); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 0d18cfd..bb8a4be 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -8,15 +8,33 @@ stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b3 app.frontend-host=${FE_URL:http://localhost:4200} spring.application.name=casino +#client registration configuration -# JWT Configuration -jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437} -jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000} +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} -# Logging +# 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.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 diff --git a/frontend/bun.lock b/frontend/bun.lock index 70cba54..aaaffeb 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -21,8 +21,11 @@ "@tailwindcss/postcss": "^4.0.3", "ajv": "8.17.1", "ajv-formats": "3.0.1", + "angular-oauth2-oidc": "^19.0.0", "countup.js": "^2.8.0", "gsap": "^3.12.7", + "keycloak-angular": "^19.0.0", + "keycloak-js": "^26.0.0", "postcss": "^8.5.1", "rxjs": "~7.8.2", "tailwindcss": "^4.0.3", @@ -754,6 +757,8 @@ "angular-eslint": ["angular-eslint@19.3.0", "", { "dependencies": { "@angular-devkit/core": ">= 19.0.0 < 20.0.0", "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", "@angular-eslint/builder": "19.3.0", "@angular-eslint/eslint-plugin": "19.3.0", "@angular-eslint/eslint-plugin-template": "19.3.0", "@angular-eslint/schematics": "19.3.0", "@angular-eslint/template-parser": "19.3.0", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } }, "sha512-19hkkH3z/2wGhKk3LfttEBkl6CtQP/tFK6/mJoO/MbIkXV0SSJWtbPbOpEaxICLlfCw0oR6W9OoQqByWkwXjkQ=="], + "angular-oauth2-oidc": ["angular-oauth2-oidc@19.0.0", "", { "dependencies": { "tslib": "^2.5.2" }, "peerDependencies": { "@angular/common": ">=19.0.0", "@angular/core": ">=19.0.0" } }, "sha512-EogHyF7MpCJSjSKIyVmdB8pJu7dU5Ilj9VNVSnFbLng4F77PIlaE4egwKUlUvk0i4ZvmO9rLXNQCm05R7Tyhcw=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -1270,6 +1275,10 @@ "karma-source-map-support": ["karma-source-map-support@1.4.0", "", { "dependencies": { "source-map-support": "^0.5.5" } }, "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A=="], + "keycloak-angular": ["keycloak-angular@19.0.2", "", { "dependencies": { "tslib": "^2.3.1" }, "peerDependencies": { "@angular/common": "^19", "@angular/core": "^19", "@angular/router": "^19", "keycloak-js": "^18 || ^19 || ^20 || ^21 || ^22 || ^23 || ^24 || ^25 || ^26" } }, "sha512-GzQKC/jFJLZRmUxWOEXkla+6shDAZFAOe6Z3qsw916Ckb/UhZnO704HMZrd8xyVB3RH6xOcNCp45oHmIiqJ7dA=="], + + "keycloak-js": ["keycloak-js@26.1.4", "", {}, "sha512-4h2RicCzIAtsjKIG8DIO+8NKlpWX2fiNkbS0jlbtjZFbIGGjbQBzjS/5NkyWlzxamXVow9prHTIgIiwfo3GAmQ=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], diff --git a/frontend/package.json b/frontend/package.json index ab8328e..16ced84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,9 @@ "ajv-formats": "3.0.1", "countup.js": "^2.8.0", "gsap": "^3.12.7", + "angular-oauth2-oidc": "^19.0.0", + "keycloak-angular": "^19.0.0", + "keycloak-js": "^26.0.0", "postcss": "^8.5.1", "rxjs": "~7.8.2", "tailwindcss": "^4.0.3", diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 30666e9..0d82bed 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; -import { FooterComponent } from '@shared/components/footer/footer.component'; +import { FooterComponent } from './shared/components/footer/footer.component'; @Component({ selector: 'app-root', diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 679c5e4..d6ac35a 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -5,6 +5,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { routes } from './app.routes'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc'; import { httpInterceptor } from '@shared/interceptor/http.interceptor'; export const appConfig: ApplicationConfig = { @@ -14,5 +15,10 @@ export const appConfig: ApplicationConfig = { provideHttpClient(withInterceptors([httpInterceptor])), provideExperimentalZonelessChangeDetection(), provideAnimationsAsync(), + provideOAuthClient(), + { + provide: OAuthStorage, + useFactory: () => localStorage, + }, ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 7958ffb..a207b91 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -8,14 +8,8 @@ export const routes: Routes = [ component: LandingComponent, }, { - 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: 'auth/callback', + loadComponent: () => import('./feature/login-success/login-success.component'), }, { path: 'home', @@ -30,6 +24,7 @@ export const routes: Routes = [ { path: 'game/slots', loadComponent: () => import('./feature/game/slots/slots.component'), + canActivate: [authGuard], }, { path: 'game/lootboxes', diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html deleted file mode 100644 index c41002d..0000000 --- a/frontend/src/app/feature/auth/login/login.component.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
- -
diff --git a/frontend/src/app/feature/auth/login/login.component.ts b/frontend/src/app/feature/auth/login/login.component.ts deleted file mode 100644 index c4cfc4c..0000000 --- a/frontend/src/app/feature/auth/login/login.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -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'; -import { NavbarComponent } from '@shared/components/navbar/navbar.component'; - -@Component({ - selector: 'app-login', - standalone: true, - imports: [CommonModule, ReactiveFormsModule, RouterLink, NavbarComponent], - templateUrl: './login.component.html', -}) -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.'; - }, - }); - } -} diff --git a/frontend/src/app/feature/auth/register/register.component.html b/frontend/src/app/feature/auth/register/register.component.html deleted file mode 100644 index 3dbeeb7..0000000 --- a/frontend/src/app/feature/auth/register/register.component.html +++ /dev/null @@ -1,102 +0,0 @@ - - -
- -
diff --git a/frontend/src/app/feature/auth/register/register.component.ts b/frontend/src/app/feature/auth/register/register.component.ts deleted file mode 100644 index 4c60c4f..0000000 --- a/frontend/src/app/feature/auth/register/register.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -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'; -import { NavbarComponent } from '@shared/components/navbar/navbar.component'; - -@Component({ - selector: 'app-register', - standalone: true, - imports: [CommonModule, ReactiveFormsModule, RouterLink, NavbarComponent], - templateUrl: './register.component.html', -}) -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: () => { - 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.'; - }, - }); - } -} diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 2763b55..a2e760b 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -51,7 +51,7 @@ export default class BlackjackComponent implements OnInit { debtAmount = signal(0); ngOnInit(): void { - this.userService.getCurrentUser().subscribe((user) => { + this.userService.currentUser$.subscribe((user) => { if (user) { this.balance.set(user.balance); } diff --git a/frontend/src/app/feature/game/slots/slots.component.ts b/frontend/src/app/feature/game/slots/slots.component.ts index ad73a03..19ffb57 100644 --- a/frontend/src/app/feature/game/slots/slots.component.ts +++ b/frontend/src/app/feature/game/slots/slots.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { HttpClient } from '@angular/common/http'; -import { KeyValuePipe, UpperCasePipe } from '@angular/common'; +import { KeyValuePipe, NgClass, UpperCasePipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; interface SlotResult { @@ -13,7 +13,7 @@ interface SlotResult { @Component({ selector: 'app-slots', standalone: true, - imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, FormsModule], + imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, NgClass, FormsModule], templateUrl: './slots.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/frontend/src/app/feature/landing/landing.component.html b/frontend/src/app/feature/landing/landing.component.html index 1e5627d..bffa9ad 100644 --- a/frontend/src/app/feature/landing/landing.component.html +++ b/frontend/src/app/feature/landing/landing.component.html @@ -10,29 +10,9 @@
200% bis zu 500€

+ 200 Freispiele

-
- @if (authService.isLoggedIn()) { - - Spiele - - } @else { - - Konto erstellen - - - Anmelden - - } -
+
diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index a3b77e3..915547f 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -1,20 +1,17 @@ -import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from '@angular/core'; +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'; -import { AuthService } from '@service/auth.service'; @Component({ selector: 'app-landing-page', standalone: true, - imports: [NavbarComponent, NgFor, RouterLink], + imports: [NavbarComponent, NgFor], templateUrl: './landing.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class LandingComponent implements OnInit, OnDestroy { currentSlide = 0; private autoplayInterval: ReturnType | undefined; - authService: AuthService = inject(AuthService); ngOnInit() { this.startAutoplay(); diff --git a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts index 3d84d9d..c1255ef 100644 --- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts +++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component } from '@angular/core'; +import { Component, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { LootboxService } from '../services/lootbox.service'; diff --git a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts index 747e29e..bc3023c 100644 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts +++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { LootboxService } from '../services/lootbox.service'; diff --git a/frontend/src/app/feature/lootboxes/services/lootbox.service.ts b/frontend/src/app/feature/lootboxes/services/lootbox.service.ts index 1d1661a..b0fb58d 100644 --- a/frontend/src/app/feature/lootboxes/services/lootbox.service.ts +++ b/frontend/src/app/feature/lootboxes/services/lootbox.service.ts @@ -1,6 +1,6 @@ -import { inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { catchError, Observable } from 'rxjs'; +import { Observable, catchError } from 'rxjs'; import { LootBox, Reward } from 'app/model/LootBox'; @Injectable({ diff --git a/frontend/src/app/model/User.ts b/frontend/src/app/model/User.ts index 3c983b8..03480a9 100644 --- a/frontend/src/app/model/User.ts +++ b/frontend/src/app/model/User.ts @@ -1,6 +1,5 @@ export interface User { - id: number; - email: string; + authentikId: string; username: string; balance: number; } diff --git a/frontend/src/app/model/auth/AuthResponse.ts b/frontend/src/app/model/auth/AuthResponse.ts deleted file mode 100644 index 495d0cd..0000000 --- a/frontend/src/app/model/auth/AuthResponse.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AuthResponse { - token: string; - tokenType: string; -} diff --git a/frontend/src/app/model/auth/LoginRequest.ts b/frontend/src/app/model/auth/LoginRequest.ts deleted file mode 100644 index 98c1b0e..0000000 --- a/frontend/src/app/model/auth/LoginRequest.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface LoginRequest { - usernameOrEmail: string; - password: string; -} diff --git a/frontend/src/app/model/auth/RegisterRequest.ts b/frontend/src/app/model/auth/RegisterRequest.ts deleted file mode 100644 index 2b07d41..0000000 --- a/frontend/src/app/model/auth/RegisterRequest.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RegisterRequest { - email: string; - username: string; - password: string; -} diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 47e1fd3..7300a25 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -1,97 +1,208 @@ -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 { inject, Injectable } from '@angular/core'; +import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; +import { UserService } from './user.service'; import { User } from '../model/User'; -import { environment } from '@environments/environment'; - -const TOKEN_KEY = 'token'; -const USER_KEY = 'user'; +import { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; +import { catchError, from, of } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthService { - private authUrl = `${environment.apiUrl}/auth`; - private userUrl = `${environment.apiUrl}/users`; + 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 currentUserSubject: BehaviorSubject; - public currentUser: Observable; + private userService: UserService = inject(UserService); + private oauthService: OAuthService = inject(OAuthService); + private router: Router = inject(Router); - constructor( - private http: HttpClient, - private router: Router - ) { - this.currentUserSubject = new BehaviorSubject(this.getUserFromStorage()); - this.currentUser = this.currentUserSubject.asObservable(); + private user: User | null = null; - // Check if token exists and load user data - if (this.getToken()) { - this.loadCurrentUser(); + 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(); } } - public get currentUserValue(): User | null { - return this.currentUserSubject.value; - } - - login(loginRequest: LoginRequest): Observable { - return this.http.post(`${this.authUrl}/login`, loginRequest).pipe( - tap((response) => { - this.setToken(response.token); - this.loadCurrentUser(); + private processCodeFlow() { + this.oauthService + .tryLogin({ + onTokenReceived: () => { + this.handleSuccessfulLogin(); + }, }) - ); + .catch((err) => { + console.error('Error processing code flow:', err); + }); } - register(registerRequest: RegisterRequest): Observable { - return this.http.post(`${this.authUrl}/register`, registerRequest); + private checkExistingSession() { + this.oauthService + .loadDiscoveryDocumentAndTryLogin() + .then((isLoggedIn) => { + if (isLoggedIn && !this.user) { + this.handleSuccessfulLogin(); + } + }) + .catch((err) => { + console.error('Error during initial login attempt:', err); + }); } - logout(): void { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USER_KEY); - this.currentUserSubject.next(null); - this.router.navigate(['/']); + private setupEventHandling() { + this.oauthService.events.subscribe((event: OAuthEvent) => { + if (event.type === 'token_received') { + this.handleSuccessfulLogin(); + } + }); } - isLoggedIn(): boolean { - return !!this.getToken(); + 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(['/']); + } + } } - getToken(): string | null { - return localStorage.getItem(TOKEN_KEY); - } - - public loadCurrentUser(): void { - this.http.get(`${this.userUrl}/me`).subscribe({ + private processUserProfile(profile: unknown) { + this.fromUserProfile(profile as Record).subscribe({ next: (user) => { - this.setUser(user); + this.user = user; + this.router.navigate(['home']); }, - error: () => { - this.logout(); + error: (err) => { + console.error('Error creating/retrieving user:', err); + if (this.oauthService.hasValidAccessToken()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } }, }); } - private setToken(token: string): void { - localStorage.setItem(TOKEN_KEY, token); + login() { + try { + this.oauthService + .loadDiscoveryDocument() + .then(() => { + this.oauthService.initLoginFlow(); + }) + .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; + } } - private setUser(user: User): void { - localStorage.setItem(USER_KEY, JSON.stringify(user)); - this.currentUserSubject.next(user); + 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(['/']); + } } - private getUserFromStorage(): User | null { - const user = localStorage.getItem(USER_KEY); - return user ? JSON.parse(user) : null; + isLoggedIn() { + return this.oauthService.hasValidAccessToken(); } - getUser(): User | null { - return this.currentUserValue; + private fromUserProfile(profile: Record) { + return this.userService.getOrCreateUser(profile); + } + + getAccessToken() { + return this.oauthService.getAccessToken(); + } + + getUser() { + return this.user; } } diff --git a/frontend/src/app/service/user.service.ts b/frontend/src/app/service/user.service.ts index d7199b3..a76e7b4 100644 --- a/frontend/src/app/service/user.service.ts +++ b/frontend/src/app/service/user.service.ts @@ -2,19 +2,28 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs'; import { User } from '../model/User'; -import { AuthService } from '@service/auth.service'; @Injectable({ providedIn: 'root', }) export class UserService { - public currentUserSubject = new BehaviorSubject(null); - public currentUser$ = this.currentUserSubject.asObservable(); private http: HttpClient = inject(HttpClient); - private authService = inject(AuthService); + private currentUserSubject = new BehaviorSubject(null); + public currentUser$ = this.currentUserSubject.asObservable(); + + constructor() { + this.getCurrentUser().subscribe(); + } + + public getUser(id: string): Observable { + return this.http.get(`/backend/user/${id}`).pipe( + catchError(() => EMPTY), + tap((user) => this.currentUserSubject.next(user)) + ); + } public getCurrentUser(): Observable { - return this.http.get('/backend/users/me').pipe( + return this.http.get('/backend/user').pipe( catchError(() => EMPTY), tap((user) => this.currentUserSubject.next(user)) ); @@ -22,7 +31,6 @@ export class UserService { public refreshCurrentUser(): void { this.getCurrentUser().subscribe(); - this.authService.loadCurrentUser(); } public updateLocalBalance(amount: number): void { @@ -35,4 +43,29 @@ export class UserService { this.currentUserSubject.next(updatedUser); } } + + public createUser(id: string, username: string): Observable { + return this.http + .post('/backend/user', { + authentikId: id, + username: username, + }) + .pipe(tap((user) => this.currentUserSubject.next(user))); + } + + public getOrCreateUser(profile: Record): Observable { + const info = profile['info'] as Record | 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); + } } diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index 90b63eb..e9f8e91 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -12,12 +12,7 @@