diff --git a/backend/requests/user.http b/backend/requests/user.http index 5e7aa5c..b594398 100644 --- a/backend/requests/user.http +++ b/backend/requests/user.http @@ -12,7 +12,7 @@ Content-Type: application/json Authorization: Bearer {{token}} { - "keycloakId": "52cc0208-a3bd-4367-94c5-0404b016a003", + "authentikId": "52cc0208-a3bd-4367-94c5-0404b016a003", "username": "john.doe" } 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 62ab40a..cdf883d 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -47,8 +47,8 @@ public class DepositController { public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException { Stripe.apiKey = stripeKey; - KeycloakUserDto userData = getKeycloakUserInfo(token); - Optional optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub()); + KeycloakUserDto userData = getAuthentikUserInfo(token); + Optional optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub()); SessionCreateParams params = SessionCreateParams.builder() .addLineItem(SessionCreateParams.LineItem.builder() @@ -77,10 +77,10 @@ public class DepositController { return ResponseEntity.ok(new SessionIdDto(session.getId())); } - private KeycloakUserDto getKeycloakUserInfo(String token) { + private KeycloakUserDto getAuthentikUserInfo(String token) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", token); - ResponseEntity response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + 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/security/CustomJwtAuthenticationConverter.java b/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java new file mode 100644 index 0000000..9f5304e --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/CustomJwtAuthenticationConverter.java @@ -0,0 +1,24 @@ +package de.szut.casino.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +public class CustomJwtAuthenticationConverter implements Converter { + + @Override + public AbstractAuthenticationToken convert(Jwt source) { + JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + + return converter.convert(source); + } + + public Converter andThen(Converter after) { + return Converter.super.andThen(after); + } +} + diff --git a/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java b/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java deleted file mode 100644 index 5e08794..0000000 --- a/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.szut.casino.security; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -@Slf4j -@Component -public class KeycloakLogoutHandler implements LogoutHandler { - - - private final RestTemplate restTemplate; - - public KeycloakLogoutHandler(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { - logout(request, auth); - } - - public void logout(HttpServletRequest request, Authentication auth) { - logoutFromKeycloak((OidcUser) auth.getPrincipal()); - } - - private void logoutFromKeycloak(OidcUser user) { - String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout"; - UriComponentsBuilder builder = UriComponentsBuilder - .fromUriString(endSessionEndpoint) - .queryParam("id_token_hint", user.getIdToken().getTokenValue()); - - ResponseEntity logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class); - if (logoutResponse.getStatusCode().is2xxSuccessful()) { - log.info("Successfulley logged out from Keycloak"); - } else { - log.error("Could not propagate logout to Keycloak"); - } - } - -} diff --git a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java b/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java deleted file mode 100644 index 48c04aa..0000000 --- a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java +++ /dev/null @@ -1,82 +0,0 @@ -package de.szut.casino.security; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -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.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.session.SessionRegistry; -import org.springframework.security.core.session.SessionRegistryImpl; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.session.HttpSessionEventPublisher; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Configuration -@EnableWebSecurity -class KeycloakSecurityConfig { - - private final KeycloakLogoutHandler keycloakLogoutHandler; - - KeycloakSecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) { - this.keycloakLogoutHandler = keycloakLogoutHandler; - } - - @Bean - public SessionRegistry sessionRegistry() { - return new SessionRegistryImpl(); - } - - @Bean - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new RegisterSessionAuthenticationStrategy(sessionRegistry()); - } - - @Bean - public HttpSessionEventPublisher httpSessionEventPublisher() { - return new HttpSessionEventPublisher(); - } - - - @Bean - public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { - http.csrf(csrf -> csrf - .ignoringRequestMatchers("/webhook") - ) - .authorizeHttpRequests(auth -> auth - .requestMatchers(HttpMethod.POST, "/webhook").permitAll() - .requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll() - .anyRequest().authenticated() - ) - .oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults())); - - return http.build(); - } - - @Bean - public JwtAuthenticationConverter jwtAuthenticationConverter() { - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { - List grantedAuthorities = new ArrayList<>(); - - Map realmAccess = jwt.getClaim("realm_access"); - if (realmAccess != null && realmAccess.containsKey("roles")) { - List roles = (List) realmAccess.get("roles"); - for (String role : roles) { - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role)); - } - } - - return grantedAuthorities; - }); - return jwtAuthenticationConverter; - } -} diff --git a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java new file mode 100644 index 0000000..5b4f4fc --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java @@ -0,0 +1,49 @@ +package de.szut.casino.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> { + auth.requestMatchers("/swagger/**", "/swagger-ui/**", "/health").permitAll() + .anyRequest().authenticated(); + }) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> + jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter()) + )); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:4200")); + 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.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} + 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 3750c8b..5daf7ef 100644 --- a/backend/src/main/java/de/szut/casino/user/UserController.java +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -5,7 +5,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -23,20 +22,13 @@ public class UserController { @Autowired private UserService userService; - @GetMapping("/user/{id}") - public ResponseEntity getUser(@PathVariable String id) { - if (id == null || !userService.exists(id)) { - return ResponseEntity.notFound().build(); - } - - return ResponseEntity.ok(userService.getUser(id)); - } - @PostMapping("/user") public ResponseEntity createUser(@RequestBody @Valid CreateUserDto userData) { - if (userService.exists(userData.getKeycloakId())) { + if (userService.exists(userData.getAuthentikId())) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Location", "/user"); - return this.redirect("/user/" + userData.getKeycloakId()); + return new ResponseEntity<>(headers, HttpStatus.FOUND); } return ResponseEntity.ok(userService.createUser(userData)); @@ -52,11 +44,4 @@ public class UserController { return ResponseEntity.ok(userData); } - - private ResponseEntity redirect(String route) { - HttpHeaders headers = new HttpHeaders(); - headers.add("Location", route); - - return new ResponseEntity<>(headers, HttpStatus.FOUND); - } } 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 c0cc92f..67fc1ae 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -19,14 +19,14 @@ public class UserEntity { @GeneratedValue private Long id; @Column(unique = true) - private String keycloakId; + private String authentikId; private String username; @Column(precision = 19, scale = 2) private BigDecimal balance; - public UserEntity(String keycloakId, String username, BigDecimal balance) { - this.keycloakId = keycloakId; + public UserEntity(String authentikId, String username, BigDecimal balance) { + this.authentikId = authentikId; this.username = username; 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 80f5546..86a1331 100644 --- a/backend/src/main/java/de/szut/casino/user/UserMappingService.java +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -9,10 +9,11 @@ import java.math.BigDecimal; @Service public class UserMappingService { public GetUserDto mapToGetUserDto(UserEntity user) { - return new GetUserDto(user.getKeycloakId(), user.getUsername(), user.getBalance()); + return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance()); } public UserEntity mapToUserEntity(CreateUserDto createUserDto) { - return new UserEntity(createUserDto.getKeycloakId(), createUserDto.getUsername(), BigDecimal.ZERO); } + 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 aaa5752..1f8d64e 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -8,8 +8,8 @@ import java.util.Optional; @Service public interface UserRepository extends JpaRepository { - @Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1") - Optional findOneByKeycloakId(String keycloakId); + @Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1") + Optional findOneByAuthentikId(String authentikId); - boolean existsByKeycloakId(String keycloakId); + 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 36569e6..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,7 +1,8 @@ package de.szut.casino.user; -import java.util.Optional; - +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; @@ -10,9 +11,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import de.szut.casino.user.dto.CreateUserDto; -import de.szut.casino.user.dto.GetUserDto; -import de.szut.casino.user.dto.KeycloakUserDto; +import java.util.Optional; @Service public class UserService { @@ -32,41 +31,51 @@ public class UserService { return user; } - public GetUserDto getUser(String keycloakId) { - Optional user = this.userRepository.findOneByKeycloakId(keycloakId); + public GetUserDto getUser(String authentikId) { + Optional user = this.userRepository.findOneByAuthentikId(authentikId); return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); } public GetUserDto getCurrentUserAsDto(String token) { - KeycloakUserDto userData = getKeycloakUserInfo(token); + KeycloakUserDto userData = getAuthentikUserInfo(token); if (userData == null) { return null; } - Optional user = this.userRepository.findOneByKeycloakId(userData.getSub()); + Optional user = this.userRepository.findOneByAuthentikId(userData.getSub()); return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); } public Optional getCurrentUser(String token) { - KeycloakUserDto userData = getKeycloakUserInfo(token); + KeycloakUserDto userData = getAuthentikUserInfo(token); if (userData == null) { return Optional.empty(); } - return this.userRepository.findOneByKeycloakId(userData.getSub()); + return this.userRepository.findOneByAuthentikId(userData.getSub()); } - private KeycloakUserDto getKeycloakUserInfo(String token) { - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", token); - ResponseEntity response = this.http.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); - - return response.getBody(); + 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 keycloakId) { - return userRepository.existsByKeycloakId(keycloakId); + 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 ff28427..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 @@ -10,6 +10,6 @@ import lombok.Setter; @AllArgsConstructor @NoArgsConstructor public class CreateUserDto { - private String keycloakId; + private String authentikId; private String username; } 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 8521029..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,7 +12,7 @@ import java.math.BigDecimal; @AllArgsConstructor @NoArgsConstructor public class GetUserDto { - private String keycloakId; + private String authentikId; private String username; private BigDecimal balance; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index a02bc80..4c56a9d 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -9,16 +9,31 @@ app.frontend-host=http://localhost:4200 spring.application.name=lf12_starter #client registration configuration -spring.security.oauth2.client.registration.keycloak.client-id=lf12 -spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.keycloak.scope=openid + +spring.security.oauth2.client.registration.authentik.client-id=MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm +spring.security.oauth2.client.registration.authentik.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} + +# Provider settings +spring.security.oauth2.client.provider.authentik.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/ +spring.security.oauth2.client.provider.authentik.authorization-uri=https://oauth.simonis.lol/application/o/authorize/ +spring.security.oauth2.client.provider.authentik.token-uri=https://oauth.simonis.lol/application/o/token/ +spring.security.oauth2.client.provider.authentik.user-info-uri=https://oauth.simonis.lol/application/o/userinfo/ +spring.security.oauth2.client.provider.authentik.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/ +spring.security.oauth2.client.provider.authentik.user-name-attribute=preferred_username + +# Resource server config +spring.security.oauth2.resourceserver.jwt.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/ +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/ #OIDC provider configuration: -spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:9090/realms/LF12 -spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username logging.level.org.springframework.security=DEBUG -#validating JWT token against our Keycloak server -spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/LF12 +#validating JWT token against our Authentik server springdoc.swagger-ui.path=swagger springdoc.swagger-ui.try-it-out-enabled=true diff --git a/backend/src/test/java/de/szut/casino/user/UserControllerTest.java b/backend/src/test/java/de/szut/casino/user/UserControllerTest.java new file mode 100644 index 0000000..57eeaea --- /dev/null +++ b/backend/src/test/java/de/szut/casino/user/UserControllerTest.java @@ -0,0 +1,122 @@ +package de.szut.casino.user; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; + +@WebMvcTest(UserController.class) +@AutoConfigureMockMvc(addFilters = false) +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserService userService; + + private GetUserDto getUserDto; + private CreateUserDto createUserDto; + private UserEntity testUser; + private final String TEST_ID = "test-id-123"; + private final String AUTH_TOKEN = "Bearer test-token"; + + @BeforeEach + void setUp() { + getUserDto = new GetUserDto(); + getUserDto.setAuthentikId(TEST_ID); + getUserDto.setUsername("testuser"); + + testUser = new UserEntity(); + testUser.setAuthentikId(TEST_ID); + testUser.setUsername("testuser"); + + createUserDto = new CreateUserDto(); + createUserDto.setAuthentikId(TEST_ID); + createUserDto.setUsername("testuser"); + } + + @Test + void getUserByIdSuccess() throws Exception { + when(userService.exists(TEST_ID)).thenReturn(true); + when(userService.getUser(TEST_ID)).thenReturn(getUserDto); + + mockMvc.perform(get("/user/" + TEST_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + void getUserByIdNotFound() throws Exception { + when(userService.exists(TEST_ID)).thenReturn(false); + + mockMvc.perform(get("/user/" + TEST_ID)) + .andExpect(status().isNotFound()); + } + + @Test + void createUserSuccess() throws Exception { + when(userService.exists(TEST_ID)).thenReturn(false); + when(userService.createUser(any(CreateUserDto.class))).thenReturn(testUser); + + mockMvc.perform(post("/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createUserDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + void createUserAlreadyExists() throws Exception { + when(userService.exists(TEST_ID)).thenReturn(true); + + mockMvc.perform(post("/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createUserDto))) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/user/" + TEST_ID)); + } + + @Test + void getCurrentUserSuccess() throws Exception { + when(userService.getCurrentUser(AUTH_TOKEN)).thenReturn(getUserDto); + + mockMvc.perform(get("/user") + .header("Authorization", AUTH_TOKEN)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + void getCurrentUserNotFound() throws Exception { + when(userService.getCurrentUser(anyString())).thenReturn(null); + + mockMvc.perform(get("/user") + .header("Authorization", AUTH_TOKEN)) + .andExpect(status().isNotFound()); + } +} \ No newline at end of file diff --git a/frontend/bun.lock b/frontend/bun.lock index e51c957..e8c14d8 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -21,6 +21,7 @@ "@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", @@ -54,15 +55,15 @@ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], - "@angular-devkit/architect": ["@angular-devkit/architect@0.1902.5", "", { "dependencies": { "@angular-devkit/core": "19.2.5", "rxjs": "7.8.1" } }, "sha512-GdcTqwCZT0CTagUoTmq799hpnbQeICx53+eHsfs+lyKjkojk1ahC6ZOi4nNLDl/J2DIMFPHIG1ZgHPuhjKItAw=="], + "@angular-devkit/architect": ["@angular-devkit/architect@0.1902.6", "", { "dependencies": { "@angular-devkit/core": "19.2.6", "rxjs": "7.8.1" } }, "sha512-Dx6yPxpaE5AhP6UtrVRDCc9Ihq9B65LAbmIh3dNOyeehratuaQS0TYNKjbpaevevJojW840DTg80N+CrlfYp9g=="], - "@angular-devkit/build-angular": ["@angular-devkit/build-angular@19.2.5", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.1902.5", "@angular-devkit/build-webpack": "0.1902.5", "@angular-devkit/core": "19.2.5", "@angular/build": "19.2.5", "@babel/core": "7.26.10", "@babel/generator": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-transform-async-generator-functions": "7.26.8", "@babel/plugin-transform-async-to-generator": "7.25.9", "@babel/plugin-transform-runtime": "7.26.10", "@babel/preset-env": "7.26.9", "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.6.3", "@ngtools/webpack": "19.2.5", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "browserslist": "^4.21.5", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild-wasm": "0.25.1", "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.3", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", "less": "4.2.2", "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.3.1", "mini-css-extract-plugin": "2.9.2", "open": "10.1.0", "ora": "5.4.1", "picomatch": "4.0.2", "piscina": "4.8.0", "postcss": "8.5.2", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", "sass": "1.85.0", "sass-loader": "16.0.5", "semver": "7.7.1", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", "terser": "5.39.0", "tree-kill": "1.2.2", "tslib": "2.8.1", "webpack": "5.98.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.2.0", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, "optionalDependencies": { "esbuild": "0.25.1" }, "peerDependencies": { "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", "@angular/ssr": "^19.2.5", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.9" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "@web/test-runner", "browser-sync", "jest", "jest-environment-jsdom", "karma", "ng-packagr", "protractor", "tailwindcss"] }, "sha512-PmLAaPuruTzEACsVe7MVyDuShQhyFdj83gWqvPKXVd8p2SIEE8SeVXyNRKNYf84cZdxqJB+IgjyvTPK7R7a+rA=="], + "@angular-devkit/build-angular": ["@angular-devkit/build-angular@19.2.6", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.1902.6", "@angular-devkit/build-webpack": "0.1902.6", "@angular-devkit/core": "19.2.6", "@angular/build": "19.2.6", "@babel/core": "7.26.10", "@babel/generator": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-transform-async-generator-functions": "7.26.8", "@babel/plugin-transform-async-to-generator": "7.25.9", "@babel/plugin-transform-runtime": "7.26.10", "@babel/preset-env": "7.26.9", "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.6.3", "@ngtools/webpack": "19.2.6", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "browserslist": "^4.21.5", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild-wasm": "0.25.1", "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.3", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", "less": "4.2.2", "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.3.1", "mini-css-extract-plugin": "2.9.2", "open": "10.1.0", "ora": "5.4.1", "picomatch": "4.0.2", "piscina": "4.8.0", "postcss": "8.5.2", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", "sass": "1.85.0", "sass-loader": "16.0.5", "semver": "7.7.1", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", "terser": "5.39.0", "tree-kill": "1.2.2", "tslib": "2.8.1", "webpack": "5.98.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.2.0", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, "optionalDependencies": { "esbuild": "0.25.1" }, "peerDependencies": { "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", "@angular/ssr": "^19.2.6", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.9" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "@web/test-runner", "browser-sync", "jest", "jest-environment-jsdom", "karma", "ng-packagr", "protractor", "tailwindcss"] }, "sha512-alYn3PSsiQML9PzU1VKbmYnIP2ULK/AqfjdeJFh8r6m8ZjUvX1zDy9TdAfC6fykQ2mGHyChteRckbx9uVOyhwQ=="], - "@angular-devkit/build-webpack": ["@angular-devkit/build-webpack@0.1902.5", "", { "dependencies": { "@angular-devkit/architect": "0.1902.5", "rxjs": "7.8.1" }, "peerDependencies": { "webpack": "^5.30.0", "webpack-dev-server": "^5.0.2" } }, "sha512-rXvUKRAgjhHTmBVr4HbZs+gS6sQ5EM+sv+Ygzl7oz7xC2+JOKBYiq+9B8Udk4GnW3Es9m6Dq7G4XbBMPzVia3Q=="], + "@angular-devkit/build-webpack": ["@angular-devkit/build-webpack@0.1902.6", "", { "dependencies": { "@angular-devkit/architect": "0.1902.6", "rxjs": "7.8.1" }, "peerDependencies": { "webpack": "^5.30.0", "webpack-dev-server": "^5.0.2" } }, "sha512-SZe2Nk39lJIJmtXWU+zhKaFy0xoU8N7387bvjhO0AoNQeRBaaJ5SrRLXX2jUzGUuVgGVF+plaVooKrmEOeM6ug=="], - "@angular-devkit/core": ["@angular-devkit/core@19.2.5", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-s5d6ZQmut5QO7pcxssIoDgeVhVEjoQKxWpBeqsSdYxMYjROMR+QnlNcyiSDLI6Wc7QR9mZINOpx8yoj6Nim1Rw=="], + "@angular-devkit/core": ["@angular-devkit/core@19.2.6", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ=="], - "@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.5", "", { "dependencies": { "@angular-devkit/core": "19.2.5", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-gfWnbwDOuKyRZK0biVyiNIhV6kmI1VmHg1LLbJm3QK6jDL0JgXD0NudgL8ILl5Ksd1sJOwQAuzTLM5iPfB3hDA=="], + "@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.6", "", { "dependencies": { "@angular-devkit/core": "19.2.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ=="], "@angular-eslint/builder": ["@angular-eslint/builder@19.3.0", "", { "dependencies": { "@angular-devkit/architect": ">= 0.1900.0 < 0.2000.0", "@angular-devkit/core": ">= 19.0.0 < 20.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-j9xNrzZJq29ONSG6EaeQHve0Squkm6u6Dm8fZgWP7crTFOrtLXn7Wxgxuyl9eddpbWY1Ov1gjFuwBVnxIdyAqg=="], @@ -78,29 +79,29 @@ "@angular-eslint/utils": ["@angular-eslint/utils@19.3.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.3.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-ovvbQh96FIJfepHqLCMdKFkPXr3EbcvYc9kMj9hZyIxs/9/VxwPH7x25mMs4VsL6rXVgH2FgG5kR38UZlcTNNw=="], - "@angular/animations": ["@angular/animations@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "19.2.4" } }, "sha512-aoVgPGaB/M9OLGt9rMMYd8V9VNzVEFQHKpyuEl4FDBoeuIaFJcXFTfwY3+L5Ew6wcIErKH67rRYJsKv8r5Ou8w=="], + "@angular/animations": ["@angular/animations@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.5", "@angular/core": "19.2.5" } }, "sha512-m4RtY3z1JuHFCh6OrOHxo25oKEigBDdR/XmdCfXIwfTiObZzNA7VQhysgdrb9IISO99kXbjZUYKDtLzgWT8Klg=="], - "@angular/build": ["@angular/build@19.2.5", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.1902.5", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-syntax-import-attributes": "7.26.0", "@inquirer/confirm": "5.1.6", "@vitejs/plugin-basic-ssl": "1.2.0", "beasties": "0.2.0", "browserslist": "^4.23.0", "esbuild": "0.25.1", "fast-glob": "3.3.3", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "listr2": "8.2.5", "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "4.0.2", "piscina": "4.8.0", "rollup": "4.34.8", "sass": "1.85.0", "semver": "7.7.1", "source-map-support": "0.5.21", "vite": "6.2.3", "watchpack": "2.4.2" }, "optionalDependencies": { "lmdb": "3.2.6" }, "peerDependencies": { "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", "@angular/ssr": "^19.2.5", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.9" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "karma", "less", "ng-packagr", "postcss", "tailwindcss"] }, "sha512-WtgdBHxFVMtbLzEYf1dYJqtld282aXxEbefsRi3RZWnLya8qO33bKMxpcd0V2iLIuIc1v/sUXPIzbWLO10mvTg=="], + "@angular/build": ["@angular/build@19.2.6", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.1902.6", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-syntax-import-attributes": "7.26.0", "@inquirer/confirm": "5.1.6", "@vitejs/plugin-basic-ssl": "1.2.0", "beasties": "0.2.0", "browserslist": "^4.23.0", "esbuild": "0.25.1", "fast-glob": "3.3.3", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "listr2": "8.2.5", "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "4.0.2", "piscina": "4.8.0", "rollup": "4.34.8", "sass": "1.85.0", "semver": "7.7.1", "source-map-support": "0.5.21", "vite": "6.2.4", "watchpack": "2.4.2" }, "optionalDependencies": { "lmdb": "3.2.6" }, "peerDependencies": { "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", "@angular/ssr": "^19.2.6", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "typescript": ">=5.5 <5.9" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "karma", "less", "ng-packagr", "postcss", "tailwindcss"] }, "sha512-+VBLb4ZPLswwJmgfsTFzGex+Sq/WveNc+uaIWyHYjwnuI17NXe1qAAg1rlp72CqGn0cirisfOyAUwPc/xZAgTg=="], - "@angular/cdk": ["@angular/cdk@19.2.7", "", { "dependencies": { "parse5": "^7.1.2", "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-+Dx1WGEWMO3OYDKr2w/Z5xOCsdjkRuG7Z18ve8eeBOHayRaC0KbYoXkvPxUiJo233CJWEzKQ/qF13C54GGWnng=="], + "@angular/cdk": ["@angular/cdk@19.2.8", "", { "dependencies": { "parse5": "^7.1.2", "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-ZZqWVYFF80TdjWkk2sc9Pn2luhiYeC78VH3Yjeln4wXMsTGDsvKPBcuOxSxxpJ31saaVBehDjBUuXMqGRj8KuA=="], - "@angular/cli": ["@angular/cli@19.2.5", "", { "dependencies": { "@angular-devkit/architect": "0.1902.5", "@angular-devkit/core": "19.2.5", "@angular-devkit/schematics": "19.2.5", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", "@schematics/angular": "19.2.5", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "8.2.5", "npm-package-arg": "12.0.2", "npm-pick-manifest": "10.0.0", "pacote": "20.0.0", "resolve": "1.22.10", "semver": "7.7.1", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, "bin": { "ng": "bin/ng.js" } }, "sha512-jiaYtbRdrGGgMQ+Qw68so7m4ZoSblz1Q27ucaFMdKZhzi9yLsWoo9bCpzIk2B7K3dG/VebbjvjLf5WOdKI8UWQ=="], + "@angular/cli": ["@angular/cli@19.2.6", "", { "dependencies": { "@angular-devkit/architect": "0.1902.6", "@angular-devkit/core": "19.2.6", "@angular-devkit/schematics": "19.2.6", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", "@schematics/angular": "19.2.6", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "8.2.5", "npm-package-arg": "12.0.2", "npm-pick-manifest": "10.0.0", "pacote": "20.0.0", "resolve": "1.22.10", "semver": "7.7.1", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, "bin": { "ng": "bin/ng.js" } }, "sha512-eZhFOSsDUHKaciwcWdU5C54ViAvPPdZJf42So93G2vZWDtEq6Uk47huocn1FY9cMhDvURfYLNrrLMpUDtUSsSA=="], - "@angular/common": ["@angular/common@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "19.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-5iBerI1hkY8rAt0gZQgOlfzR69jj5j25JyfkDOhdZhezE0pqhDc69OnbkUM20LTau4bFRYOj015eiKWzE2DOzQ=="], + "@angular/common": ["@angular/common@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-vFCBdas4C5PxP6ts/4TlRddWD3DUmI3aaO0QZdZvqyLHy428t84ruYdsJXKaeD8ie2U4/9F3a1tsklclRG/BBA=="], - "@angular/compiler": ["@angular/compiler@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-HxUwmkoXMlj9EiSmRMRTI4vR3d5hSxiIZazq7OWtlEm8uKedzLzf72dF+hdc3yF6JCdF87vWiQN22bcGeTxYZw=="], + "@angular/compiler": ["@angular/compiler@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-34J+HubQjwkbZ0AUtU5sa4Zouws9XtP/fKaysMQecoYJTZ3jewzLSRu3aAEZX1Y4gIrcVVKKIxM6oWoXKwYMOA=="], - "@angular/compiler-cli": ["@angular/compiler-cli@19.2.4", "", { "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^17.2.1" }, "peerDependencies": { "@angular/compiler": "19.2.4", "typescript": ">=5.5 <5.9" }, "bin": { "ngc": "bundles/src/bin/ngc.js", "ngcc": "bundles/ngcc/index.js", "ng-xi18n": "bundles/src/bin/ng_xi18n.js" } }, "sha512-zIWWJm0L+OGMGoRJ73WW96+LDSmZsWqNpwYYXBAEzzoMtPMsWg8uiOIxxjF9ZUWQ1Y5ODUSADnBJwt5vtiLbzA=="], + "@angular/compiler-cli": ["@angular/compiler-cli@19.2.5", "", { "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^17.2.1" }, "peerDependencies": { "@angular/compiler": "19.2.5", "typescript": ">=5.5 <5.9" }, "bin": { "ngc": "bundles/src/bin/ngc.js", "ngcc": "bundles/ngcc/index.js", "ng-xi18n": "bundles/src/bin/ng_xi18n.js" } }, "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ=="], - "@angular/core": ["@angular/core@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" } }, "sha512-ZuSMg+LWG0ADLEvMzSqU+D6M5KcQtxBssEFq4UskGIYuvNGqC91hAl4sbnXDQ5C7GgFcLY6ouaemS6dBOIfc/g=="], + "@angular/core": ["@angular/core@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" } }, "sha512-NNEz1sEZz1mBpgf6Tz3aJ9b8KjqpTiMYhHfCYA9h9Ipe4D8gUmOsvPHPK2M755OX7p7PmUmzp1XCUHYrZMVHRw=="], - "@angular/forms": ["@angular/forms@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.4", "@angular/core": "19.2.4", "@angular/platform-browser": "19.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-XzFVmy2BduohtV6E304VCiCvayqV6hiYfPDvkzQnPiFfnQqRCGOTKSDOqxBDsSoDoZW7vZNHe3HmNMdyPg3Rog=="], + "@angular/forms": ["@angular/forms@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.5", "@angular/core": "19.2.5", "@angular/platform-browser": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-2Zvy3qK1kOxiAX9fdSaeG48q7oyO/4RlMYlg1w+ra9qX1SrgwF3OQ2P2Vs+ojg1AxN3z9xFp4aYaaID/G2LZAw=="], - "@angular/platform-browser": ["@angular/platform-browser@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "19.2.4", "@angular/common": "19.2.4", "@angular/core": "19.2.4" }, "optionalPeers": ["@angular/animations"] }, "sha512-skP+Oq9hxh0hkLcs2bXgnt7Z+KKP5xZYzaHPEToLtPat6l6kSPjT0CJ+DE/8ce443hItAcCbn+JrKGC29nd2pw=="], + "@angular/platform-browser": ["@angular/platform-browser@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "19.2.5", "@angular/common": "19.2.5", "@angular/core": "19.2.5" }, "optionalPeers": ["@angular/animations"] }, "sha512-Lshy++X16cvl6OPvfzMySpsqEaCPKEJmDjz7q7oSt96oxlh6LvOeOUVLjsNyrNaIt9NadpWoqjlu/I9RTPJkpw=="], - "@angular/platform-browser-dynamic": ["@angular/platform-browser-dynamic@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.4", "@angular/compiler": "19.2.4", "@angular/core": "19.2.4", "@angular/platform-browser": "19.2.4" } }, "sha512-KEVf5YTVBFrFAAW7nOVARy+A/xFJ56iDaeoqn63XB3VF5btEGpqoAxKbQGWRRB9G68uZBFXalJ9wXjS6v2T4ng=="], + "@angular/platform-browser-dynamic": ["@angular/platform-browser-dynamic@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.5", "@angular/compiler": "19.2.5", "@angular/core": "19.2.5", "@angular/platform-browser": "19.2.5" } }, "sha512-15in8u4552EcdWNTXY2h0MKuJbk3AuXwWr0zVTum4CfB/Ss2tNTrDEdWhgAbhnUI0e9jZQee/fhBbA1rleMYrA=="], - "@angular/router": ["@angular/router@19.2.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.4", "@angular/core": "19.2.4", "@angular/platform-browser": "19.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-pnQX6gk8Z+YQFtnuqRDPEv+d9Up2oP1ZJk9/i/vnYS53PguSEtKgCBuiy6FQmn7SdrYFJ3+ZoV6ow9jhv00eqA=="], + "@angular/router": ["@angular/router@19.2.5", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.5", "@angular/core": "19.2.5", "@angular/platform-browser": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-9pSfmdNXLjaOKj0kd4UxBC7sFdCFOnRGbftp397G3KWqsLsGSKmNFzqhXNeA5QHkaVxnpmpm8HzXU+zYV5JwSg=="], "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], @@ -486,7 +487,7 @@ "@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg=="], - "@ngtools/webpack": ["@ngtools/webpack@19.2.5", "", { "peerDependencies": { "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "typescript": ">=5.5 <5.9", "webpack": "^5.54.0" } }, "sha512-rp9hRFJiUzRrlUBbM3c4BSt/zB93GLM1X9eb+JQOwBsoQhRL92VU9kkffGDpK14hf6uB4goQ00AvQ4lEnxlUag=="], + "@ngtools/webpack": ["@ngtools/webpack@19.2.6", "", { "peerDependencies": { "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", "typescript": ">=5.5 <5.9", "webpack": "^5.54.0" } }, "sha512-/jWpZUoMru3YbRJAPZ2KroUSzE6Ak5Hav219raYQaBXVtyLAvFE5VC1/CiH0wTYnb/dyjxzWq38ftOr/vv0+tg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -580,7 +581,7 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="], - "@schematics/angular": ["@schematics/angular@19.2.5", "", { "dependencies": { "@angular-devkit/core": "19.2.5", "@angular-devkit/schematics": "19.2.5", "jsonc-parser": "3.3.1" } }, "sha512-LXzeWpW7vhW7zk48atwdR860hOp2xEyU+TqDUz4dcLk5sPI14x94fAJuAWch42+9/X6LnkFLB+W2CmyOY9ZD1g=="], + "@schematics/angular": ["@schematics/angular@19.2.6", "", { "dependencies": { "@angular-devkit/core": "19.2.6", "@angular-devkit/schematics": "19.2.6", "jsonc-parser": "3.3.1" } }, "sha512-fmbF9ONmEZqxHocCwOSWG2mHp4a22d1uW+DZUBUgZSBUFIrnFw42deOxDq8mkZOZ1Tc73UpLN2GKI7iJeUqS2A=="], "@sigstore/bundle": ["@sigstore/bundle@3.1.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.4.0" } }, "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag=="], @@ -650,7 +651,7 @@ "@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], @@ -662,7 +663,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="], + "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], "@types/node-forge": ["@types/node-forge@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ=="], @@ -756,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=="], @@ -830,7 +833,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001707", "", {}, "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001709", "", {}, "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -898,13 +901,8 @@ "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], -<<<<<<< HEAD -======= "countup.js": ["countup.js@2.8.0", "", {}, "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ=="], - "critters": ["critters@0.0.24", "", { "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", "postcss-media-query-parser": "^0.2.3" } }, "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q=="], - ->>>>>>> f2d447a (feat(blackjack): add animated number component and usage) "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-loader": ["css-loader@7.1.2", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA=="], @@ -959,7 +957,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.129", "", {}, "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.130", "", {}, "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1785,7 +1783,7 @@ "ua-parser-js": ["ua-parser-js@0.7.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -1821,7 +1819,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vite": ["vite@6.2.3", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg=="], + "vite": ["vite@6.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw=="], "void-elements": ["void-elements@2.0.1", "", {}, "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung=="], @@ -1975,6 +1973,8 @@ "@tufjs/models/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], diff --git a/frontend/package.json b/frontend/package.json index 2ee7ed0..9872efe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "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", diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index bbc5fb6..0d82bed 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,13 +1,12 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; -import { KeycloakAngularModule } from 'keycloak-angular'; import { FooterComponent } from './shared/components/footer/footer.component'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, KeycloakAngularModule, FooterComponent], + imports: [CommonModule, RouterOutlet, FooterComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.css', diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 217efd4..671a717 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,59 +1,24 @@ -import { - APP_INITIALIZER, - ApplicationConfig, - provideExperimentalZonelessChangeDetection, -} from '@angular/core'; +import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { routes } from './app.routes'; -import { - KeycloakAngularModule, - KeycloakBearerInterceptor, - KeycloakService, -} from 'keycloak-angular'; -import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; - -export const initializeKeycloak = (keycloak: KeycloakService) => async () => - keycloak.init({ - config: { - url: 'http://localhost:9090', - realm: 'LF12', - clientId: 'lf12', - }, - loadUserProfileAtStartUp: true, - initOptions: { - onLoad: 'check-sso', - silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', - checkLoginIframe: false, - redirectUri: window.location.origin + '/', - }, - }); - -function initializeApp(keycloak: KeycloakService): () => Promise { - return () => initializeKeycloak(keycloak)(); -} +import { OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc'; +import { httpInterceptor } from './shared/interceptor/http.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - KeycloakAngularModule, FontAwesomeModule, - { - provide: APP_INITIALIZER, - useFactory: initializeApp, - multi: true, - deps: [KeycloakService], - }, - KeycloakService, - provideHttpClient(withInterceptorsFromDi()), + provideHttpClient(withInterceptors([httpInterceptor])), provideExperimentalZonelessChangeDetection(), - { - provide: HTTP_INTERCEPTORS, - useClass: KeycloakBearerInterceptor, - multi: true, - }, provideAnimationsAsync(), + provideOAuthClient(), + { + provide: OAuthStorage, + useFactory: () => localStorage, + }, ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index e783513..c536f8f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -8,7 +8,7 @@ export const routes: Routes = [ component: LandingComponent, }, { - path: 'login/success', + path: 'auth/callback', loadComponent: () => import('./feature/login-success/login-success.component'), }, { diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts index 035ccc8..d088c30 100644 --- a/frontend/src/app/auth.guard.ts +++ b/frontend/src/app/auth.guard.ts @@ -1,12 +1,12 @@ import { CanActivateFn, Router } from '@angular/router'; import { inject } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import { AuthService } from './service/auth.service'; export const authGuard: CanActivateFn = async () => { - const keycloakService = inject(KeycloakService); + const authService = inject(AuthService); const router = inject(Router); - if (keycloakService.isLoggedIn()) { + if (authService.isLoggedIn()) { return true; } diff --git a/frontend/src/app/feature/deposit/deposit.component.ts b/frontend/src/app/feature/deposit/deposit.component.ts index 6cbae07..834a2e4 100644 --- a/frontend/src/app/feature/deposit/deposit.component.ts +++ b/frontend/src/app/feature/deposit/deposit.component.ts @@ -17,7 +17,7 @@ import { import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { loadStripe, Stripe } from '@stripe/stripe-js'; import { debounceTime } from 'rxjs'; -import { NgIf } from '@angular/common'; +import { CommonModule } from '@angular/common'; import gsap from 'gsap'; import { DepositService } from '@service/deposit.service'; import { environment } from '@environments/environment'; @@ -26,7 +26,7 @@ import { ModalAnimationService } from '@shared/services/modal-animation.service' @Component({ selector: 'app-deposit', standalone: true, - imports: [ReactiveFormsModule, NgIf], + imports: [ReactiveFormsModule, CommonModule], templateUrl: './deposit.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/frontend/src/app/feature/login-success/login-success.component.ts b/frontend/src/app/feature/login-success/login-success.component.ts index 3b506ca..d10a322 100644 --- a/frontend/src/app/feature/login-success/login-success.component.ts +++ b/frontend/src/app/feature/login-success/login-success.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; import { Router } from '@angular/router'; -import { UserService } from '@service/user.service'; +import { AuthService } from '../../service/auth.service'; +import { OAuthService } from 'angular-oauth2-oidc'; @Component({ selector: 'app-login-success', @@ -12,15 +12,32 @@ import { UserService } from '@service/user.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export default class LoginSuccessComponent implements OnInit { - private userService: UserService = inject(UserService); - private keycloakService: KeycloakService = inject(KeycloakService); + private authService: AuthService = inject(AuthService); + private oauthService: OAuthService = inject(OAuthService); private router: Router = inject(Router); async ngOnInit() { - const userProfile = await this.keycloakService.loadUserProfile(); - const user = await this.userService.getOrCreateUser(userProfile); - sessionStorage.setItem('user', JSON.stringify(user)); - - this.router.navigate(['home']); + try { + if (this.oauthService.hasValidAccessToken()) { + this.router.navigate(['/home']); + } else { + setTimeout(() => { + if (this.oauthService.hasValidAccessToken() || this.authService.getUser()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } + }, 3000); + } + } catch (err) { + console.error('Error during login callback:', err); + setTimeout(() => { + if (this.authService.isLoggedIn()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } + }, 3000); + } } } diff --git a/frontend/src/app/model/User.ts b/frontend/src/app/model/User.ts index a579b7a..03480a9 100644 --- a/frontend/src/app/model/User.ts +++ b/frontend/src/app/model/User.ts @@ -1,5 +1,5 @@ export interface User { - keycloakId: string; + authentikId: string; username: string; balance: number; } diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts new file mode 100644 index 0000000..7300a25 --- /dev/null +++ b/frontend/src/app/service/auth.service.ts @@ -0,0 +1,208 @@ +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 { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; +import { catchError, from, of } from 'rxjs'; + +@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 processCodeFlow() { + this.oauthService + .tryLogin({ + onTokenReceived: () => { + this.handleSuccessfulLogin(); + }, + }) + .catch((err) => { + console.error('Error processing code flow:', err); + }); + } + + 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).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(); + }) + .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(['/']); + } + } + + isLoggedIn() { + return this.oauthService.hasValidAccessToken(); + } + + 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 1e801c6..5f668c2 100644 --- a/frontend/src/app/service/user.service.ts +++ b/frontend/src/app/service/user.service.ts @@ -1,6 +1,5 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { KeycloakProfile } from 'keycloak-js'; import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs'; import { User } from '../model/User'; @@ -37,24 +36,25 @@ export class UserService { public createUser(id: string, username: string): Observable { return this.http .post('/backend/user', { - keycloakId: id, + authentikId: id, username: username, }) .pipe(tap((user) => this.currentUserSubject.next(user))); } - public async getOrCreateUser(userProfile: KeycloakProfile) { - if (userProfile.id == null) { - return; - } - return await this.getUser(userProfile.id) - .toPromise() - .then(async (user) => { - if (user) { - return 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); - return await this.createUser(userProfile.id ?? '', userProfile.username ?? '').toPromise(); - }); + 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.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index f1fffba..6bbeeb9 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -7,7 +7,7 @@ import { signal, } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { KeycloakService } from 'keycloak-angular'; +import { AuthService } from '../../../service/auth.service'; import { CurrencyPipe } from '@angular/common'; import { UserService } from '@service/user.service'; import { Subscription } from 'rxjs'; @@ -22,8 +22,8 @@ import { AnimatedNumberComponent } from '@blackjack/components/animated-number/a }) export class NavbarComponent implements OnInit, OnDestroy { isMenuOpen = false; - private keycloakService: KeycloakService = inject(KeycloakService); - isLoggedIn = this.keycloakService.isLoggedIn(); + private authService: AuthService = inject(AuthService); + isLoggedIn = this.authService.isLoggedIn(); private userService = inject(UserService); private userSubscription: Subscription | undefined; @@ -43,15 +43,14 @@ export class NavbarComponent implements OnInit, OnDestroy { login() { try { - const baseUrl = window.location.origin; - this.keycloakService.login({ redirectUri: `${baseUrl}/login/success` }); + this.authService.login(); } catch (error) { console.error('Login failed:', error); } } logout() { - this.keycloakService.logout(); + this.authService.logout(); } toggleMenu() { diff --git a/frontend/src/app/shared/interceptor/http.interceptor.ts b/frontend/src/app/shared/interceptor/http.interceptor.ts new file mode 100644 index 0000000..f2737e8 --- /dev/null +++ b/frontend/src/app/shared/interceptor/http.interceptor.ts @@ -0,0 +1,21 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { OAuthStorage } from 'angular-oauth2-oidc'; + +export const httpInterceptor: HttpInterceptorFn = (req, next) => { + const oauthStorage = inject(OAuthStorage); + + if (oauthStorage.getItem('access_token')) { + return next( + req.clone({ + setHeaders: { + Authorization: 'Bearer ' + oauthStorage.getItem('access_token'), + 'Access-Control-Allow-Origin': '*', + 'Referrer-Policy': 'no-referrer', + }, + }) + ); + } else { + return next(req); + } +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 53866cc..f691e8e 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -1,4 +1,7 @@ export const environment = { STRIPE_KEY: 'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG', + OAUTH_CLIENT_ID: 'MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm', + OAUTH_CLIENT_SECRET: + 'GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5', };