Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
9ec6d8f413 | |||
f680a8829c | |||
b803055307 | |||
c942b4bb39 | |||
c765ef87e3 | |||
02453449cd | |||
25c68e230d | |||
87c822dbd7 | |||
b5a6582905 | |||
28f7b15d4c | |||
4b70a4ac4a | |||
64f701c651 | |||
0e1946d190 | |||
9de08ab233 | |||
2e76446328 | |||
47f4a4d558 | |||
e37dcecd3f | |||
d3b7e7d5e7 | |||
617654caeb | |||
fa09a8533f | |||
d7fe0e3965 | |||
80d5c1e413 | |||
8317349507 | |||
a2f1a40931 | |||
|
7eebd12699 | ||
3d7ee92cf2 | |||
7bec17dd52 | |||
775205b54c | |||
eb5b94c7bb | |||
40c402ae36 | |||
801edfe89e | |||
68a226b677 | |||
faa0a1495b | |||
823cb88807 | |||
0aa7ad1031 | |||
b1b8c939a6 | |||
6182ff717f | |||
4c3f42d347 | |||
9981ebc9d1 | |||
ab80f5d285 | |||
3da534f3ae | |||
|
242b72ca45 | ||
|
e848b548b5 | ||
144f033beb | |||
f547d05f64 | |||
33683f565f |
43 changed files with 1081 additions and 813 deletions
|
@ -39,7 +39,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.stripe:stripe-java:20.136.0")
|
implementation("com.stripe:stripe-java:29.0.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,10 +74,6 @@ public class BlackJackService {
|
||||||
UserEntity user = getUserWithFreshData(game.getUser());
|
UserEntity user = getUserWithFreshData(game.getUser());
|
||||||
BigDecimal additionalBet = game.getBet();
|
BigDecimal additionalBet = game.getBet();
|
||||||
|
|
||||||
if (user.getBalance().compareTo(additionalBet) < 0) {
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
deductBetFromBalance(user, additionalBet);
|
deductBetFromBalance(user, additionalBet);
|
||||||
game.setBet(game.getBet().add(additionalBet));
|
game.setBet(game.getBet().add(additionalBet));
|
||||||
|
|
||||||
|
|
|
@ -47,15 +47,19 @@ 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()
|
||||||
.setAmount((long) amountDto.getAmount() * 100)
|
.setPriceData(SessionCreateParams.LineItem.PriceData.builder()
|
||||||
.setCurrency("EUR")
|
.setCurrency("EUR")
|
||||||
|
.setUnitAmount((long) amountDto.getAmount() * 100)
|
||||||
|
.setProductData(SessionCreateParams.LineItem.PriceData.ProductData.builder()
|
||||||
|
.setName("Einzahlung")
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
.setQuantity(1L)
|
.setQuantity(1L)
|
||||||
.setName("Einzahlung")
|
|
||||||
.build())
|
.build())
|
||||||
.setSuccessUrl(frontendHost+"/home?success=true")
|
.setSuccessUrl(frontendHost+"/home?success=true")
|
||||||
.setCancelUrl(frontendHost+"/home?success=false")
|
.setCancelUrl(frontendHost+"/home?success=false")
|
||||||
|
@ -73,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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class TransactionService {
|
||||||
.build();
|
.build();
|
||||||
Session checkoutSession = Session.retrieve(sessionID, params, null);
|
Session checkoutSession = Session.retrieve(sessionID, params, null);
|
||||||
|
|
||||||
if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) {
|
if (!"paid".equals(checkoutSession.getPaymentStatus())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +53,12 @@ public class TransactionService {
|
||||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||||
|
|
||||||
UserEntity user = transaction.getUser();
|
UserEntity user = transaction.getUser();
|
||||||
user.addBalance(checkoutSession.getAmountTotal());
|
Long amountTotal = checkoutSession.getAmountTotal();
|
||||||
|
if (amountTotal != null) {
|
||||||
|
user.addBalance(amountTotal);
|
||||||
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
transactionRepository.save(transaction);
|
transactionRepository.save(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,18 @@ public class WebhookController {
|
||||||
switch (event.getType()) {
|
switch (event.getType()) {
|
||||||
case "checkout.session.completed":
|
case "checkout.session.completed":
|
||||||
case "checkout.session.async_payment_succeeded":
|
case "checkout.session.async_payment_succeeded":
|
||||||
Session session = (Session) event.getData().getObject();
|
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
|
||||||
|
|
||||||
this.transactionService.fulfillCheckout(session.getId());
|
if (dataObjectDeserializer.getObject().isPresent()) {
|
||||||
|
Session session = (Session) dataObjectDeserializer.getObject().get();
|
||||||
|
this.transactionService.fulfillCheckout(session.getId());
|
||||||
|
} else {
|
||||||
|
logger.error("Failed to deserialize webhook event data");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// No action needed for other event types
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok().body(null);
|
return ResponseEntity.ok().body(null);
|
||||||
|
|
|
@ -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;
|
||||||
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());
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -28,9 +28,11 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@stripe/stripe-js": "^7.0.0",
|
"@stripe/stripe-js": "^7.0.0",
|
||||||
"@tailwindcss/postcss": "^4.0.3",
|
"@tailwindcss/postcss": "^4.0.3",
|
||||||
"ajv": "8.8.0",
|
"ajv": "8.17.1",
|
||||||
"ajv-formats": "2.1.1",
|
"ajv-formats": "3.0.1",
|
||||||
|
"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",
|
||||||
|
@ -53,6 +55,6 @@
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "~5.8.0",
|
"typescript": "~5.8.0",
|
||||||
"typescript-eslint": "8.29.0"
|
"typescript-eslint": "8.29.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 = {
|
||||||
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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,19 +6,6 @@
|
||||||
<app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
|
<app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
|
||||||
<app-player-hand [cards]="playerCards()"></app-player-hand>
|
<app-player-hand [cards]="playerCards()"></app-player-hand>
|
||||||
|
|
||||||
@if (isActionInProgress()) {
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div
|
|
||||||
class="card p-4 flex items-center gap-3 animate-pulse bg-deep-blue-light border border-deep-blue-light/50"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-5 h-5 rounded-full border-2 border-white border-t-transparent animate-spin"
|
|
||||||
></div>
|
|
||||||
<span>{{ currentAction() }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (gameInProgress()) {
|
@if (gameInProgress()) {
|
||||||
<app-game-controls
|
<app-game-controls
|
||||||
[playerCards]="playerCards()"
|
[playerCards]="playerCards()"
|
||||||
|
@ -44,10 +31,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Result Modal -->
|
|
||||||
<app-game-result
|
<app-game-result
|
||||||
[gameState]="gameState()"
|
[gameState]="gameState()"
|
||||||
[amount]="currentBet()"
|
[amount]="currentBet()"
|
||||||
|
[balance]="balance()"
|
||||||
[show]="showGameResult()"
|
[show]="showGameResult()"
|
||||||
(gameResultClosed)="onCloseGameResult()"
|
(gameResultClosed)="onCloseGameResult()"
|
||||||
></app-game-result>
|
></app-game-result>
|
||||||
|
|
||||||
|
<app-debt-dialog
|
||||||
|
[amount]="debtAmount()"
|
||||||
|
[show]="showDebtDialog()"
|
||||||
|
(dialogClosed)="onCloseDebtDialog()"
|
||||||
|
></app-debt-dialog>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { GameState } from '@blackjack/enum/gameState';
|
||||||
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
import { UserService } from '@service/user.service';
|
import { UserService } from '@service/user.service';
|
||||||
import { timer } from 'rxjs';
|
import { timer } from 'rxjs';
|
||||||
|
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blackjack',
|
selector: 'app-blackjack',
|
||||||
|
@ -27,6 +28,7 @@ import { timer } from 'rxjs';
|
||||||
GameControlsComponent,
|
GameControlsComponent,
|
||||||
GameInfoComponent,
|
GameInfoComponent,
|
||||||
GameResultComponent,
|
GameResultComponent,
|
||||||
|
DebtDialogComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './blackjack.component.html',
|
templateUrl: './blackjack.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
@ -46,7 +48,9 @@ export default class BlackjackComponent implements OnInit {
|
||||||
showGameResult = signal(false);
|
showGameResult = signal(false);
|
||||||
|
|
||||||
isActionInProgress = signal(false);
|
isActionInProgress = signal(false);
|
||||||
currentAction = signal<string>('');
|
|
||||||
|
showDebtDialog = signal(false);
|
||||||
|
debtAmount = signal(0);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.userService.currentUser$.subscribe((user) => {
|
this.userService.currentUser$.subscribe((user) => {
|
||||||
|
@ -91,7 +95,6 @@ export default class BlackjackComponent implements OnInit {
|
||||||
|
|
||||||
onNewGame(bet: number): void {
|
onNewGame(bet: number): void {
|
||||||
this.isActionInProgress.set(true);
|
this.isActionInProgress.set(true);
|
||||||
this.currentAction.set('Spiel wird gestartet...');
|
|
||||||
|
|
||||||
this.blackjackService.startGame(bet).subscribe({
|
this.blackjackService.startGame(bet).subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
|
@ -110,7 +113,6 @@ export default class BlackjackComponent implements OnInit {
|
||||||
if (!this.currentGameId() || this.isActionInProgress()) return;
|
if (!this.currentGameId() || this.isActionInProgress()) return;
|
||||||
|
|
||||||
this.isActionInProgress.set(true);
|
this.isActionInProgress.set(true);
|
||||||
this.currentAction.set('Karte wird gezogen...');
|
|
||||||
|
|
||||||
this.blackjackService.hit(this.currentGameId()!).subscribe({
|
this.blackjackService.hit(this.currentGameId()!).subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
|
@ -137,7 +139,6 @@ export default class BlackjackComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isActionInProgress.set(true);
|
this.isActionInProgress.set(true);
|
||||||
this.currentAction.set('Dealer zieht Karten...');
|
|
||||||
|
|
||||||
this.blackjackService.stand(this.currentGameId()!).subscribe({
|
this.blackjackService.stand(this.currentGameId()!).subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
|
@ -162,12 +163,16 @@ export default class BlackjackComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isActionInProgress.set(true);
|
this.isActionInProgress.set(true);
|
||||||
this.currentAction.set('Einsatz wird verdoppelt...');
|
|
||||||
|
|
||||||
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
|
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
this.updateGameState(game);
|
this.updateGameState(game);
|
||||||
this.userService.refreshCurrentUser();
|
this.userService.getCurrentUser().subscribe((user) => {
|
||||||
|
if (user && user.balance < 0) {
|
||||||
|
this.debtAmount.set(Math.abs(user.balance));
|
||||||
|
this.showDebtDialog.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.isActionInProgress.set(false);
|
this.isActionInProgress.set(false);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
@ -184,6 +189,10 @@ export default class BlackjackComponent implements OnInit {
|
||||||
this.userService.refreshCurrentUser();
|
this.userService.refreshCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCloseDebtDialog(): void {
|
||||||
|
this.showDebtDialog.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
private handleGameError(error: HttpErrorResponse): void {
|
private handleGameError(error: HttpErrorResponse): void {
|
||||||
if (error instanceof HttpErrorResponse) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
if (error.status === 400 && error.error?.error === 'Invalid state') {
|
if (error.status === 400 && error.error?.error === 'Invalid state') {
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
SimpleChanges,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
AfterViewInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||||
|
import { CountUp } from 'countup.js';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-animated-number',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, CurrencyPipe],
|
||||||
|
template: ` <span #numberElement>{{ formattedValue }}</span> `,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
|
||||||
|
@Input() value = 0;
|
||||||
|
@Input() duration = 1;
|
||||||
|
@Input() ease = 'power1.out';
|
||||||
|
|
||||||
|
@ViewChild('numberElement') numberElement!: ElementRef;
|
||||||
|
|
||||||
|
private countUp: CountUp | null = null;
|
||||||
|
private previousValue = 0;
|
||||||
|
formattedValue = '0,00 €';
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.initializeCountUp();
|
||||||
|
if (this.countUp && this.value !== 0) {
|
||||||
|
this.countUp.start(() => {
|
||||||
|
this.previousValue = this.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['value']) {
|
||||||
|
if (this.countUp) {
|
||||||
|
const endVal = this.value;
|
||||||
|
|
||||||
|
this.countUp.update(endVal);
|
||||||
|
this.previousValue = endVal;
|
||||||
|
} else {
|
||||||
|
this.formattedValue = new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeCountUp(): void {
|
||||||
|
if (this.numberElement) {
|
||||||
|
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
|
||||||
|
startVal: this.previousValue,
|
||||||
|
duration: this.duration,
|
||||||
|
easingFn: (t, b, c, d) => {
|
||||||
|
if (this.ease === 'power1.out') {
|
||||||
|
return c * (1 - Math.pow(1 - t / d, 1)) + b;
|
||||||
|
}
|
||||||
|
return c * (t / d) + b;
|
||||||
|
},
|
||||||
|
formattingFn: (value) => {
|
||||||
|
const formatted = new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(value);
|
||||||
|
this.formattedValue = formatted;
|
||||||
|
return formatted;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Card } from '@blackjack/models/blackjack.model';
|
import { Card } from '@blackjack/models/blackjack.model';
|
||||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||||
|
import { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dealer-hand',
|
selector: 'app-dealer-hand',
|
||||||
|
@ -9,7 +10,15 @@ import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||||
imports: [CommonModule, PlayingCardComponent],
|
imports: [CommonModule, PlayingCardComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="section-heading text-2xl mb-4">Croupier's Karten</h3>
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="section-heading text-2xl">Dealer's Karten</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="text-text-secondary">Punkte:</div>
|
||||||
|
<div class="text-xl font-bold text-accent-red">
|
||||||
|
{{ gameControlsService.calculateHandValue(cards) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card p-6 !bg-accent-red">
|
<div class="card p-6 !bg-accent-red">
|
||||||
<div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg">
|
<div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg">
|
||||||
@if (cards.length > 0) {
|
@if (cards.length > 0) {
|
||||||
|
@ -38,6 +47,8 @@ export class DealerHandComponent implements OnChanges {
|
||||||
|
|
||||||
private lastCardCount = 0;
|
private lastCardCount = 0;
|
||||||
|
|
||||||
|
constructor(protected gameControlsService: GameControlsService) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes['cards']) {
|
if (changes['cards']) {
|
||||||
this.updateCardsWithState();
|
this.updateCardsWithState();
|
||||||
|
|
|
@ -28,54 +28,27 @@ import { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||||
(click)="hit.emit()"
|
(click)="hit.emit()"
|
||||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||||
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
||||||
[class.opacity-50]="isActionInProgress"
|
|
||||||
>
|
>
|
||||||
<span [class.invisible]="isActionInProgress">Ziehen</span>
|
<span>Ziehen</span>
|
||||||
@if (isActionInProgress) {
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="stand.emit()"
|
(click)="stand.emit()"
|
||||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||||
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
||||||
[class.opacity-50]="isActionInProgress"
|
|
||||||
>
|
>
|
||||||
<span [class.invisible]="isActionInProgress">Halten</span>
|
<span>Halten</span>
|
||||||
@if (isActionInProgress) {
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="doubleDown.emit()"
|
(click)="doubleDown.emit()"
|
||||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||||
[disabled]="
|
[disabled]="!canDoubleDown || isActionInProgress"
|
||||||
gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress
|
|
||||||
"
|
|
||||||
[class.opacity-50]="isActionInProgress"
|
|
||||||
>
|
>
|
||||||
<span [class.invisible]="isActionInProgress">Verdoppeln</span>
|
<span>Verdoppeln</span>
|
||||||
@if (isActionInProgress) {
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="leave.emit()"
|
(click)="leave.emit()"
|
||||||
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
|
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
|
||||||
[disabled]="isActionInProgress"
|
[disabled]="isActionInProgress"
|
||||||
[class.opacity-50]="isActionInProgress"
|
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
|
@ -97,4 +70,12 @@ export class GameControlsComponent {
|
||||||
protected readonly GameState = GameState;
|
protected readonly GameState = GameState;
|
||||||
|
|
||||||
constructor(protected gameControlsService: GameControlsService) {}
|
constructor(protected gameControlsService: GameControlsService) {}
|
||||||
|
|
||||||
|
get canDoubleDown(): boolean {
|
||||||
|
return (
|
||||||
|
this.gameState === GameState.IN_PROGRESS &&
|
||||||
|
this.playerCards.length === 2 &&
|
||||||
|
!this.isActionInProgress
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,12 @@ import {
|
||||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BettingService } from '@blackjack/services/betting.service';
|
import { BettingService } from '@blackjack/services/betting.service';
|
||||||
|
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-game-info',
|
selector: 'app-game-info',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule],
|
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule, AnimatedNumberComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
|
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
|
||||||
|
@ -23,7 +24,7 @@ import { BettingService } from '@blackjack/services/betting.service';
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-text-secondary">Aktuelle Wette:</span>
|
<span class="text-text-secondary">Aktuelle Wette:</span>
|
||||||
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
|
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
|
||||||
{{ currentBet | currency: 'EUR' }}
|
<app-animated-number [value]="currentBet" [duration]="0.5"></app-animated-number>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from
|
||||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
import { GameState } from '../../enum/gameState';
|
import { GameState } from '../../enum/gameState';
|
||||||
|
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-game-result',
|
selector: 'app-game-result',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, CurrencyPipe],
|
imports: [CommonModule, CurrencyPipe, AnimatedNumberComponent],
|
||||||
template: `
|
template: `
|
||||||
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
|
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
|
||||||
<div class="modal-card" [@cardAnimation]>
|
<div class="modal-card" [@cardAnimation]>
|
||||||
|
@ -18,7 +19,9 @@ import { GameState } from '../../enum/gameState';
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="text-text-secondary">Einsatz:</div>
|
<div class="text-text-secondary">Einsatz:</div>
|
||||||
<div class="font-medium text-right">{{ amount }} €</div>
|
<div class="font-medium text-right">
|
||||||
|
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-text-secondary">
|
<div class="text-text-secondary">
|
||||||
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
|
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
|
||||||
|
@ -31,21 +34,22 @@ import { GameState } from '../../enum/gameState';
|
||||||
'text-yellow-400': isDraw,
|
'text-yellow-400': isDraw,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ isLoss ? '-' : '+' }}{{ amount }} €
|
{{ isLoss ? '-' : '+' }}
|
||||||
|
<app-animated-number
|
||||||
|
[value]="isWin ? amount * 2 : amount"
|
||||||
|
[duration]="0.5"
|
||||||
|
></app-animated-number>
|
||||||
|
<div *ngIf="isWin" class="text-xs text-text-secondary">
|
||||||
|
(Einsatz
|
||||||
|
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> × 2)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-text-secondary border-t border-text-secondary/20 pt-3 font-medium">
|
<div class="text-text-secondary border-t border-text-secondary/20 pt-3 font-medium">
|
||||||
Gesamt:
|
Kontostand:
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="font-medium text-right border-t border-text-secondary/20 pt-3">
|
||||||
class="font-medium text-right border-t border-text-secondary/20 pt-3"
|
<app-animated-number [value]="balance" [duration]="0.5"></app-animated-number>
|
||||||
[ngClass]="{
|
|
||||||
'text-emerald': isWin,
|
|
||||||
'text-accent-red': isLoss,
|
|
||||||
'text-white': isDraw,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ isWin ? '+' : isLoss ? '-' : '' }}{{ amount }} €
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,6 +80,7 @@ import { GameState } from '../../enum/gameState';
|
||||||
export class GameResultComponent {
|
export class GameResultComponent {
|
||||||
@Input() gameState: GameState = GameState.IN_PROGRESS;
|
@Input() gameState: GameState = GameState.IN_PROGRESS;
|
||||||
@Input() amount = 0;
|
@Input() amount = 0;
|
||||||
|
@Input() balance = 0;
|
||||||
@Input() set show(value: boolean) {
|
@Input() set show(value: boolean) {
|
||||||
console.log('GameResultComponent show input changed:', value, 'gameState:', this.gameState);
|
console.log('GameResultComponent show input changed:', value, 'gameState:', this.gameState);
|
||||||
this.visible = value;
|
this.visible = value;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||||
import { Card } from '@blackjack/models/blackjack.model';
|
import { Card } from '@blackjack/models/blackjack.model';
|
||||||
|
import { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-player-hand',
|
selector: 'app-player-hand',
|
||||||
|
@ -9,7 +10,15 @@ import { Card } from '@blackjack/models/blackjack.model';
|
||||||
imports: [CommonModule, PlayingCardComponent],
|
imports: [CommonModule, PlayingCardComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="section-heading text-2xl mb-4">Deine Karten</h3>
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="section-heading text-2xl">Deine Karten</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="text-text-secondary">Punkte:</div>
|
||||||
|
<div class="text-xl font-bold text-emerald">
|
||||||
|
{{ gameControlsService.calculateHandValue(cards) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card p-6 !bg-emerald">
|
<div class="card p-6 !bg-emerald">
|
||||||
<div
|
<div
|
||||||
class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
|
class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
|
||||||
|
@ -40,6 +49,8 @@ export class PlayerHandComponent implements OnChanges {
|
||||||
|
|
||||||
private lastCardCount = 0;
|
private lastCardCount = 0;
|
||||||
|
|
||||||
|
constructor(protected gameControlsService: GameControlsService) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes['cards']) {
|
if (changes['cards']) {
|
||||||
this.updateCardsWithState();
|
this.updateCardsWithState();
|
||||||
|
|
|
@ -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 {
|
||||||
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=');
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -13,7 +12,6 @@ export class UserService {
|
||||||
public currentUser$ = this.currentUserSubject.asObservable();
|
public currentUser$ = this.currentUserSubject.asObservable();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize with current user data
|
|
||||||
this.getCurrentUser().subscribe();
|
this.getCurrentUser().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,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);
|
||||||
}
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { interval, Subscription, takeWhile } from 'rxjs';
|
||||||
|
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-debt-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, AnimatedNumberComponent],
|
||||||
|
template: `
|
||||||
|
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
|
||||||
|
<div class="modal-card" [@cardAnimation]>
|
||||||
|
<h2 class="modal-heading text-accent-red">WARNUNG!</h2>
|
||||||
|
<p class="py-2 text-text-secondary mb-4">
|
||||||
|
Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns
|
||||||
|
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>.
|
||||||
|
</p>
|
||||||
|
<p class="py-2 text-accent-red mb-4 font-bold">
|
||||||
|
Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben!
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="bg-deep-blue-light/50 rounded-lg p-5 mb-6 shadow-inner border border-deep-blue-light/30"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="text-text-secondary">Schulden:</div>
|
||||||
|
<div class="font-medium text-right text-accent-red">
|
||||||
|
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div
|
||||||
|
class="text-8xl font-bold text-accent-red"
|
||||||
|
[class.animate-pulse]="timeLeft() <= 10"
|
||||||
|
[class.animate-bounce]="timeLeft() <= 5"
|
||||||
|
[@countdown]="timeLeft()"
|
||||||
|
>
|
||||||
|
{{ timeLeft() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-text-secondary mt-2">Sekunden verbleibend</div>
|
||||||
|
</div>
|
||||||
|
@if (timeLeft() === 0) {
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 bg-accent-red/20 blur-xl rounded-full"></div>
|
||||||
|
<div
|
||||||
|
class="relative bg-gradient-to-b from-accent-red to-red-900 p-8 rounded-lg border-2 border-accent-red shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center gap-4 mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="text-4xl font-black tracking-wider text-white animate-[pulse_1s_ease-in-out_infinite]"
|
||||||
|
>
|
||||||
|
ZEIT ABGELAUFEN
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold text-white/90 tracking-wider animate-[pulse_1s_ease-in-out_infinite]"
|
||||||
|
>
|
||||||
|
KONSEQUENZEN FOLGEN
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button type="button" (click)="closeDialog()" class="button-primary w-full py-2">
|
||||||
|
Verstanden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
trigger('fadeInOut', [
|
||||||
|
transition(':enter', [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate('150ms ease-out', style({ opacity: 1 })),
|
||||||
|
]),
|
||||||
|
transition(':leave', [animate('150ms ease-in', style({ opacity: 0 }))]),
|
||||||
|
]),
|
||||||
|
trigger('cardAnimation', [
|
||||||
|
transition(':enter', [
|
||||||
|
style({ opacity: 0, transform: 'scale(0.95)' }),
|
||||||
|
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
trigger('countdown', [
|
||||||
|
transition('* => *', [
|
||||||
|
style({ transform: 'scale(1.2)' }),
|
||||||
|
animate('100ms ease-out', style({ transform: 'scale(1)' })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DebtDialogComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() amount = 0;
|
||||||
|
@Input() set show(value: boolean) {
|
||||||
|
this.visible = value;
|
||||||
|
if (value) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output() dialogClosed = new EventEmitter<void>();
|
||||||
|
|
||||||
|
visible = false;
|
||||||
|
timeLeft = signal(30);
|
||||||
|
private timerSubscription: Subscription | undefined;
|
||||||
|
private warningSound = new Audio('assets/sounds/warning.mp3');
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.visible) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
this.timeLeft.set(30);
|
||||||
|
this.timerSubscription = interval(1000)
|
||||||
|
.pipe(takeWhile(() => this.timeLeft() > 0))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.timeLeft.update((value) => value - 1);
|
||||||
|
if (this.timeLeft() <= 5) {
|
||||||
|
this.warningSound.play();
|
||||||
|
}
|
||||||
|
if (this.timeLeft() === 0) {
|
||||||
|
setTimeout(() => this.closeDialog(), 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer() {
|
||||||
|
if (this.timerSubscription) {
|
||||||
|
this.timerSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog(): void {
|
||||||
|
this.stopTimer();
|
||||||
|
this.visible = false;
|
||||||
|
this.dialogClosed.emit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,11 @@
|
||||||
@if (isLoggedIn) {
|
@if (isLoggedIn) {
|
||||||
<div
|
<div
|
||||||
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
|
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
|
||||||
|
routerLink="/home"
|
||||||
>
|
>
|
||||||
<span>{{ balance() | currency: 'EUR' : 'symbol' : '1.2-2' }}</span>
|
<span [class]="balance() < 0 ? 'text-accent-red' : ''">
|
||||||
|
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,22 +7,23 @@ 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';
|
||||||
|
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterModule, CurrencyPipe],
|
imports: [RouterModule, CurrencyPipe, AnimatedNumberComponent],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
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;
|
||||||
|
@ -42,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',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue