feat: add authentik for authentication #58
29 changed files with 592 additions and 304 deletions
|
@ -12,7 +12,7 @@ Content-Type: application/json
|
||||||
Authorization: Bearer {{token}}
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"keycloakId": "52cc0208-a3bd-4367-94c5-0404b016a003",
|
"authentikId": "52cc0208-a3bd-4367-94c5-0404b016a003",
|
||||||
"username": "john.doe"
|
"username": "john.doe"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,8 @@ public class DepositController {
|
||||||
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
|
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
|
||||||
Stripe.apiKey = stripeKey;
|
Stripe.apiKey = stripeKey;
|
||||||
|
|
||||||
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
KeycloakUserDto userData = getAuthentikUserInfo(token);
|
||||||
Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub());
|
Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub());
|
||||||
|
|
||||||
SessionCreateParams params = SessionCreateParams.builder()
|
SessionCreateParams params = SessionCreateParams.builder()
|
||||||
.addLineItem(SessionCreateParams.LineItem.builder()
|
.addLineItem(SessionCreateParams.LineItem.builder()
|
||||||
|
@ -77,10 +77,10 @@ public class DepositController {
|
||||||
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeycloakUserDto getKeycloakUserInfo(String token) {
|
private KeycloakUserDto getAuthentikUserInfo(String token) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.set("Authorization", token);
|
headers.set("Authorization", token);
|
||||||
ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
|
ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
|
||||||
|
|
||||||
return response.getBody();
|
return response.getBody();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Jwt, AbstractAuthenticationToken> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractAuthenticationToken convert(Jwt source) {
|
||||||
|
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||||
|
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
|
||||||
|
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
|
||||||
|
|
||||||
|
return converter.convert(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <U> Converter<Jwt, U> andThen(Converter<? super AbstractAuthenticationToken, ? extends U> after) {
|
||||||
|
return Converter.super.andThen(after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<String> 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
|
||||||
|
|
||||||
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
|
|
||||||
if (realmAccess != null && realmAccess.containsKey("roles")) {
|
|
||||||
List<String> roles = (List<String>) realmAccess.get("roles");
|
|
||||||
for (String role : roles) {
|
|
||||||
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return grantedAuthorities;
|
|
||||||
});
|
|
||||||
return jwtAuthenticationConverter;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
@ -23,20 +22,13 @@ public class UserController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
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")
|
@PostMapping("/user")
|
||||||
public ResponseEntity<?> createUser(@RequestBody @Valid CreateUserDto userData) {
|
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));
|
return ResponseEntity.ok(userService.createUser(userData));
|
||||||
|
@ -52,11 +44,4 @@ public class UserController {
|
||||||
|
|
||||||
return ResponseEntity.ok(userData);
|
return ResponseEntity.ok(userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<Object> redirect(String route) {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.add("Location", route);
|
|
||||||
|
|
||||||
return new ResponseEntity<>(headers, HttpStatus.FOUND);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,14 @@ public class UserEntity {
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private Long id;
|
||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
private String keycloakId;
|
private String authentikId;
|
||||||
jank marked this conversation as resolved
Outdated
|
|||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Column(precision = 19, scale = 2)
|
@Column(precision = 19, scale = 2)
|
||||||
private BigDecimal balance;
|
private BigDecimal balance;
|
||||||
|
|
||||||
public UserEntity(String keycloakId, String username, BigDecimal balance) {
|
public UserEntity(String authentikId, String username, BigDecimal balance) {
|
||||||
this.keycloakId = keycloakId;
|
this.authentikId = authentikId;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.balance = balance;
|
this.balance = balance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import java.math.BigDecimal;
|
||||||
@Service
|
@Service
|
||||||
public class UserMappingService {
|
public class UserMappingService {
|
||||||
public GetUserDto mapToGetUserDto(UserEntity user) {
|
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) {
|
public UserEntity mapToUserEntity(CreateUserDto createUserDto) {
|
||||||
return new UserEntity(createUserDto.getKeycloakId(), createUserDto.getUsername(), BigDecimal.ZERO); }
|
return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||||
@Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1")
|
@Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1")
|
||||||
Optional<UserEntity> findOneByKeycloakId(String keycloakId);
|
Optional<UserEntity> findOneByAuthentikId(String authentikId);
|
||||||
|
|
||||||
boolean existsByKeycloakId(String keycloakId);
|
boolean existsByAuthentikId(String authentikId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package de.szut.casino.user;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
@ -10,9 +11,7 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import java.util.Optional;
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
|
||||||
import de.szut.casino.user.dto.KeycloakUserDto;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
@ -32,41 +31,51 @@ public class UserService {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetUserDto getUser(String keycloakId) {
|
public GetUserDto getUser(String authentikId) {
|
||||||
Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(keycloakId);
|
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(authentikId);
|
||||||
|
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetUserDto getCurrentUserAsDto(String token) {
|
public GetUserDto getCurrentUserAsDto(String token) {
|
||||||
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
KeycloakUserDto userData = getAuthentikUserInfo(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(userData.getSub());
|
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(userData.getSub());
|
||||||
|
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<UserEntity> getCurrentUser(String token) {
|
public Optional<UserEntity> getCurrentUser(String token) {
|
||||||
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
KeycloakUserDto userData = getAuthentikUserInfo(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return this.userRepository.findOneByKeycloakId(userData.getSub());
|
return this.userRepository.findOneByAuthentikId(userData.getSub());
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeycloakUserDto getKeycloakUserInfo(String token) {
|
private KeycloakUserDto getAuthentikUserInfo(String token) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
try {
|
||||||
headers.set("Authorization", token);
|
HttpHeaders headers = new HttpHeaders();
|
||||||
ResponseEntity<KeycloakUserDto> response = this.http.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
|
headers.set("Authorization", token);
|
||||||
|
ResponseEntity<KeycloakUserDto> response = this.http.exchange(
|
||||||
return response.getBody();
|
"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());
|
||||||
jank marked this conversation as resolved
ptran
commented
Consider throwing an exception idk Consider throwing an exception idk
jank
commented
why tho what is that gonna do? why tho what is that gonna do?
jank
commented
nvm at that point I could also just remove the catch here nvm at that point I could also just remove the catch here
|
|||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean exists(String keycloakId) {
|
public boolean exists(String authentikId) {
|
||||||
return userRepository.existsByKeycloakId(keycloakId);
|
return userRepository.existsByAuthentikId(authentikId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@ import lombok.Setter;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class CreateUserDto {
|
public class CreateUserDto {
|
||||||
private String keycloakId;
|
private String authentikId;
|
||||||
private String username;
|
private String username;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import java.math.BigDecimal;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class GetUserDto {
|
public class GetUserDto {
|
||||||
private String keycloakId;
|
private String authentikId;
|
||||||
private String username;
|
private String username;
|
||||||
private BigDecimal balance;
|
private BigDecimal balance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,31 @@ app.frontend-host=http://localhost:4200
|
||||||
|
|
||||||
spring.application.name=lf12_starter
|
spring.application.name=lf12_starter
|
||||||
#client registration configuration
|
#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.authentik.client-id=MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm
|
||||||
spring.security.oauth2.client.registration.keycloak.scope=openid
|
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:
|
#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
|
logging.level.org.springframework.security=DEBUG
|
||||||
#validating JWT token against our Keycloak server
|
#validating JWT token against our Authentik server
|
||||||
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/LF12
|
|
||||||
|
|
||||||
springdoc.swagger-ui.path=swagger
|
springdoc.swagger-ui.path=swagger
|
||||||
springdoc.swagger-ui.try-it-out-enabled=true
|
springdoc.swagger-ui.try-it-out-enabled=true
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@
|
||||||
"@tailwindcss/postcss": "^4.0.3",
|
"@tailwindcss/postcss": "^4.0.3",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"ajv-formats": "3.0.1",
|
"ajv-formats": "3.0.1",
|
||||||
|
"angular-oauth2-oidc": "^19.0.0",
|
||||||
"countup.js": "^2.8.0",
|
"countup.js": "^2.8.0",
|
||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
"keycloak-angular": "^19.0.0",
|
"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=="],
|
"@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=="],
|
"@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-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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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": ["@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=="],
|
"@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/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=="],
|
"@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-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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"ajv-formats": "3.0.1",
|
"ajv-formats": "3.0.1",
|
||||||
"countup.js": "^2.8.0",
|
"countup.js": "^2.8.0",
|
||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
|
"angular-oauth2-oidc": "^19.0.0",
|
||||||
"keycloak-angular": "^19.0.0",
|
"keycloak-angular": "^19.0.0",
|
||||||
"keycloak-js": "^26.0.0",
|
"keycloak-js": "^26.0.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { KeycloakAngularModule } from 'keycloak-angular';
|
|
||||||
import { FooterComponent } from './shared/components/footer/footer.component';
|
import { FooterComponent } from './shared/components/footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, KeycloakAngularModule, FooterComponent],
|
imports: [CommonModule, RouterOutlet, FooterComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.css',
|
styleUrl: './app.component.css',
|
||||||
|
|
|
@ -1,59 +1,24 @@
|
||||||
import {
|
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
|
||||||
APP_INITIALIZER,
|
|
||||||
ApplicationConfig,
|
|
||||||
provideExperimentalZonelessChangeDetection,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import {
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
KeycloakAngularModule,
|
|
||||||
KeycloakBearerInterceptor,
|
|
||||||
KeycloakService,
|
|
||||||
} from 'keycloak-angular';
|
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
import { OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc';
|
||||||
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
import { httpInterceptor } from './shared/interceptor/http.interceptor';
|
||||||
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<boolean> {
|
|
||||||
return () => initializeKeycloak(keycloak)();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
jank marked this conversation as resolved
Outdated
ptran
commented
/* Example of a custom storage factory - not used in the current implementation /* Example of a custom storage factory - not used in the current implementation
???
|
|||||||
providers: [
|
providers: [
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
KeycloakAngularModule,
|
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
{
|
provideHttpClient(withInterceptors([httpInterceptor])),
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: initializeApp,
|
|
||||||
multi: true,
|
|
||||||
deps: [KeycloakService],
|
|
||||||
},
|
|
||||||
KeycloakService,
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
|
||||||
provideExperimentalZonelessChangeDetection(),
|
provideExperimentalZonelessChangeDetection(),
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: KeycloakBearerInterceptor,
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
|
provideOAuthClient(),
|
||||||
|
{
|
||||||
|
provide: OAuthStorage,
|
||||||
|
useFactory: () => localStorage,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const routes: Routes = [
|
||||||
component: LandingComponent,
|
component: LandingComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'login/success',
|
path: 'auth/callback',
|
||||||
loadComponent: () => import('./feature/login-success/login-success.component'),
|
loadComponent: () => import('./feature/login-success/login-success.component'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { AuthService } from './service/auth.service';
|
||||||
|
|
||||||
export const authGuard: CanActivateFn = async () => {
|
export const authGuard: CanActivateFn = async () => {
|
||||||
const keycloakService = inject(KeycloakService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
if (keycloakService.isLoggedIn()) {
|
if (authService.isLoggedIn()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||||
import { debounceTime } from 'rxjs';
|
import { debounceTime } from 'rxjs';
|
||||||
import { NgIf } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { DepositService } from '@service/deposit.service';
|
import { DepositService } from '@service/deposit.service';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
|
@ -26,7 +26,7 @@ import { ModalAnimationService } from '@shared/services/modal-animation.service'
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-deposit',
|
selector: 'app-deposit',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReactiveFormsModule, NgIf],
|
imports: [ReactiveFormsModule, CommonModule],
|
||||||
templateUrl: './deposit.component.html',
|
templateUrl: './deposit.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { UserService } from '@service/user.service';
|
import { AuthService } from '../../service/auth.service';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login-success',
|
selector: 'app-login-success',
|
||||||
|
@ -12,15 +12,32 @@ import { UserService } from '@service/user.service';
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export default class LoginSuccessComponent implements OnInit {
|
export default class LoginSuccessComponent implements OnInit {
|
||||||
private userService: UserService = inject(UserService);
|
private authService: AuthService = inject(AuthService);
|
||||||
private keycloakService: KeycloakService = inject(KeycloakService);
|
private oauthService: OAuthService = inject(OAuthService);
|
||||||
private router: Router = inject(Router);
|
private router: Router = inject(Router);
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const userProfile = await this.keycloakService.loadUserProfile();
|
try {
|
||||||
jank marked this conversation as resolved
ptran
commented
Remove comments Remove comments
|
|||||||
const user = await this.userService.getOrCreateUser(userProfile);
|
if (this.oauthService.hasValidAccessToken()) {
|
||||||
sessionStorage.setItem('user', JSON.stringify(user));
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
this.router.navigate(['home']);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export interface User {
|
export interface User {
|
||||||
keycloakId: string;
|
authentikId: string;
|
||||||
username: string;
|
username: string;
|
||||||
balance: number;
|
balance: number;
|
||||||
}
|
}
|
||||||
|
|
208
frontend/src/app/service/auth.service.ts
Normal file
208
frontend/src/app/service/auth.service.ts
Normal file
|
@ -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=');
|
||||||
|
|
||||||
jank marked this conversation as resolved
Outdated
ptran
commented
useless useless
|
|||||||
|
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<string, unknown>).subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.user = user;
|
||||||
|
this.router.navigate(['home']);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error creating/retrieving user:', err);
|
||||||
|
if (this.oauthService.hasValidAccessToken()) {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
try {
|
||||||
|
this.oauthService
|
||||||
|
.loadDiscoveryDocument()
|
||||||
|
.then(() => {
|
||||||
|
this.oauthService.initLoginFlow();
|
||||||
|
})
|
||||||
|
.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<string, unknown>) {
|
||||||
|
return this.userService.getOrCreateUser(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessToken() {
|
||||||
|
return this.oauthService.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { KeycloakProfile } from 'keycloak-js';
|
|
||||||
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
|
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
|
||||||
import { User } from '../model/User';
|
import { User } from '../model/User';
|
||||||
|
|
||||||
|
@ -37,24 +36,25 @@ export class UserService {
|
||||||
public createUser(id: string, username: string): Observable<User> {
|
public createUser(id: string, username: string): Observable<User> {
|
||||||
return this.http
|
return this.http
|
||||||
.post<User>('/backend/user', {
|
.post<User>('/backend/user', {
|
||||||
keycloakId: id,
|
authentikId: id,
|
||||||
username: username,
|
username: username,
|
||||||
})
|
})
|
||||||
.pipe(tap((user) => this.currentUserSubject.next(user)));
|
.pipe(tap((user) => this.currentUserSubject.next(user)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOrCreateUser(userProfile: KeycloakProfile) {
|
public getOrCreateUser(profile: Record<string, unknown>): Observable<User> {
|
||||||
if (userProfile.id == null) {
|
const info = profile['info'] as Record<string, unknown> | undefined;
|
||||||
return;
|
const id = (info?.['sub'] as string) || (profile['sub'] as string);
|
||||||
jank marked this conversation as resolved
Outdated
ptran
commented
cool bro cool bro
|
|||||||
}
|
const username =
|
||||||
return await this.getUser(userProfile.id)
|
(info?.['preferred_username'] as string) ||
|
||||||
.toPromise()
|
(profile['preferred_username'] as string) ||
|
||||||
.then(async (user) => {
|
(profile['email'] as string) ||
|
||||||
if (user) {
|
(profile['name'] as string);
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.createUser(userProfile.id ?? '', userProfile.username ?? '').toPromise();
|
if (!id || !username) {
|
||||||
});
|
throw new Error('Invalid user profile data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createUser(id, username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { AuthService } from '../../../service/auth.service';
|
||||||
import { CurrencyPipe } from '@angular/common';
|
import { CurrencyPipe } from '@angular/common';
|
||||||
import { UserService } from '@service/user.service';
|
import { UserService } from '@service/user.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
@ -22,8 +22,8 @@ import { AnimatedNumberComponent } from '@blackjack/components/animated-number/a
|
||||||
})
|
})
|
||||||
export class NavbarComponent implements OnInit, OnDestroy {
|
export class NavbarComponent implements OnInit, OnDestroy {
|
||||||
isMenuOpen = false;
|
isMenuOpen = false;
|
||||||
private keycloakService: KeycloakService = inject(KeycloakService);
|
private authService: AuthService = inject(AuthService);
|
||||||
isLoggedIn = this.keycloakService.isLoggedIn();
|
isLoggedIn = this.authService.isLoggedIn();
|
||||||
|
|
||||||
private userService = inject(UserService);
|
private userService = inject(UserService);
|
||||||
private userSubscription: Subscription | undefined;
|
private userSubscription: Subscription | undefined;
|
||||||
|
@ -43,15 +43,14 @@ export class NavbarComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
try {
|
try {
|
||||||
const baseUrl = window.location.origin;
|
this.authService.login();
|
||||||
this.keycloakService.login({ redirectUri: `${baseUrl}/login/success` });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.keycloakService.logout();
|
this.authService.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMenu() {
|
toggleMenu() {
|
||||||
|
|
21
frontend/src/app/shared/interceptor/http.interceptor.ts
Normal file
21
frontend/src/app/shared/interceptor/http.interceptor.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,7 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
STRIPE_KEY:
|
STRIPE_KEY:
|
||||||
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
|
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
|
||||||
|
OAUTH_CLIENT_ID: 'MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm',
|
||||||
|
OAUTH_CLIENT_SECRET:
|
||||||
|
'GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5',
|
||||||
};
|
};
|
||||||
|
|
Reference in a new issue
Cool info, but not needed