Compare commits

..

No commits in common. "main" and "v1.25.0" have entirely different histories.

43 changed files with 940 additions and 1452 deletions

View file

@ -39,7 +39,7 @@ repositories {
} }
dependencies { dependencies {
implementation("com.stripe:stripe-java:29.0.0") implementation("com.stripe:stripe-java:20.136.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")

View file

@ -12,7 +12,7 @@ Content-Type: application/json
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
{ {
"authentikId": "52cc0208-a3bd-4367-94c5-0404b016a003", "keycloakId": "52cc0208-a3bd-4367-94c5-0404b016a003",
"username": "john.doe" "username": "john.doe"
} }

View file

@ -74,6 +74,10 @@ 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));

View file

@ -47,19 +47,15 @@ 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 = getAuthentikUserInfo(token); KeycloakUserDto userData = getKeycloakUserInfo(token);
Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub()); Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub());
SessionCreateParams params = SessionCreateParams.builder() SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder() .addLineItem(SessionCreateParams.LineItem.builder()
.setPriceData(SessionCreateParams.LineItem.PriceData.builder() .setAmount((long) amountDto.getAmount() * 100)
.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")
@ -77,10 +73,10 @@ public class DepositController {
return ResponseEntity.ok(new SessionIdDto(session.getId())); return ResponseEntity.ok(new SessionIdDto(session.getId()));
} }
private KeycloakUserDto getAuthentikUserInfo(String token) { private KeycloakUserDto getKeycloakUserInfo(String token) {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token); headers.set("Authorization", token);
ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
return response.getBody(); return response.getBody();
} }

View file

@ -40,7 +40,7 @@ public class TransactionService {
.build(); .build();
Session checkoutSession = Session.retrieve(sessionID, params, null); Session checkoutSession = Session.retrieve(sessionID, params, null);
if (!"paid".equals(checkoutSession.getPaymentStatus())) { if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) {
return; return;
} }
@ -53,12 +53,10 @@ public class TransactionService {
transaction.setStatus(TransactionStatus.SUCCEEDED); transaction.setStatus(TransactionStatus.SUCCEEDED);
UserEntity user = transaction.getUser(); UserEntity user = transaction.getUser();
Long amountTotal = checkoutSession.getAmountTotal(); user.addBalance(checkoutSession.getAmountTotal());
if (amountTotal != null) {
user.addBalance(amountTotal);
}
userRepository.save(user); userRepository.save(user);
transactionRepository.save(transaction); transactionRepository.save(transaction);
} }
} }

View file

@ -51,18 +51,12 @@ 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":
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); Session session = (Session) event.getData().getObject();
if (dataObjectDeserializer.getObject().isPresent()) { this.transactionService.fulfillCheckout(session.getId());
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);

View file

@ -1,24 +0,0 @@
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);
}
}

View file

@ -0,0 +1,48 @@
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");
}
}
}

View file

@ -0,0 +1,82 @@
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;
}
}

View file

@ -1,49 +0,0 @@
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;
}
}

View file

@ -5,6 +5,7 @@ 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;
@ -22,13 +23,20 @@ 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.getAuthentikId())) { if (userService.exists(userData.getKeycloakId())) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", "/user");
return new ResponseEntity<>(headers, HttpStatus.FOUND); return this.redirect("/user/" + userData.getKeycloakId());
} }
return ResponseEntity.ok(userService.createUser(userData)); return ResponseEntity.ok(userService.createUser(userData));
@ -44,4 +52,11 @@ 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);
}
} }

View file

@ -19,14 +19,14 @@ public class UserEntity {
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@Column(unique = true) @Column(unique = true)
private String authentikId; private String keycloakId;
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 authentikId, String username, BigDecimal balance) { public UserEntity(String keycloakId, String username, BigDecimal balance) {
this.authentikId = authentikId; this.keycloakId = keycloakId;
this.username = username; this.username = username;
this.balance = balance; this.balance = balance;
} }

View file

@ -9,11 +9,10 @@ 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.getAuthentikId(), user.getUsername(), user.getBalance()); return new GetUserDto(user.getKeycloakId(), user.getUsername(), user.getBalance());
} }
public UserEntity mapToUserEntity(CreateUserDto createUserDto) { public UserEntity mapToUserEntity(CreateUserDto createUserDto) {
return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO); return new UserEntity(createUserDto.getKeycloakId(), createUserDto.getUsername(), BigDecimal.ZERO); }
}
} }

View file

@ -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.authentikId = ?1") @Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1")
Optional<UserEntity> findOneByAuthentikId(String authentikId); Optional<UserEntity> findOneByKeycloakId(String keycloakId);
boolean existsByAuthentikId(String authentikId); boolean existsByKeycloakId(String keycloakId);
} }

View file

@ -1,8 +1,7 @@
package de.szut.casino.user; package de.szut.casino.user;
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;
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;
@ -11,7 +10,9 @@ 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 java.util.Optional; import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import de.szut.casino.user.dto.KeycloakUserDto;
@Service @Service
public class UserService { public class UserService {
@ -31,51 +32,41 @@ public class UserService {
return user; return user;
} }
public GetUserDto getUser(String authentikId) { public GetUserDto getUser(String keycloakId) {
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(authentikId); Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(keycloakId);
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 = getAuthentikUserInfo(token); KeycloakUserDto userData = getKeycloakUserInfo(token);
if (userData == null) { if (userData == null) {
return null; return null;
} }
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(userData.getSub()); Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(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 = getAuthentikUserInfo(token); KeycloakUserDto userData = getKeycloakUserInfo(token);
if (userData == null) { if (userData == null) {
return Optional.empty(); return Optional.empty();
} }
return this.userRepository.findOneByAuthentikId(userData.getSub()); return this.userRepository.findOneByKeycloakId(userData.getSub());
} }
private KeycloakUserDto getAuthentikUserInfo(String token) { private KeycloakUserDto getKeycloakUserInfo(String token) {
try { HttpHeaders headers = new HttpHeaders();
HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", token);
headers.set("Authorization", token); ResponseEntity<KeycloakUserDto> response = this.http.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
ResponseEntity<KeycloakUserDto> response = this.http.exchange(
"https://oauth.simonis.lol/application/o/userinfo/",
HttpMethod.GET,
new HttpEntity<>(headers),
KeycloakUserDto.class
);
return response.getBody(); return response.getBody();
} catch (Exception e) {
System.err.println("Error fetching user info from Authentik: " + e.getMessage());
return null;
}
} }
public boolean exists(String authentikId) { public boolean exists(String keycloakId) {
return userRepository.existsByAuthentikId(authentikId); return userRepository.existsByKeycloakId(keycloakId);
} }
} }

View file

@ -10,6 +10,6 @@ import lombok.Setter;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class CreateUserDto { public class CreateUserDto {
private String authentikId; private String keycloakId;
private String username; private String username;
} }

View file

@ -12,7 +12,7 @@ import java.math.BigDecimal;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class GetUserDto { public class GetUserDto {
private String authentikId; private String keycloakId;
private String username; private String username;
private BigDecimal balance; private BigDecimal balance;
} }

View file

@ -9,31 +9,16 @@ 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.authentik.client-id=MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authentik.client-secret=GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5 spring.security.oauth2.client.registration.keycloak.scope=openid
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 Authentik server #validating JWT token against our Keycloak 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

View file

@ -1,122 +0,0 @@
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

View file

@ -9,43 +9,39 @@
"test": "bunx @angular/cli test", "test": "bunx @angular/cli test",
"format": "prettier --write \"src/**/*.{ts,html,css,scss}\"", "format": "prettier --write \"src/**/*.{ts,html,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"", "format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"",
"lint": "bunx @angular/cli lint" "lint": "ng lint"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.0.0", "@angular/animations": "^18.2.0",
"@angular/cdk": "~19.2.0", "@angular/cdk": "~18.2.14",
"@angular/common": "^19.0.0", "@angular/common": "^18.2.0",
"@angular/compiler": "^19.2.4", "@angular/compiler": "^18.2.0",
"@angular/core": "^19.0.0", "@angular/core": "^18.2.0",
"@angular/forms": "^19.0.0", "@angular/forms": "^18.2.0",
"@angular/platform-browser": "^19.0.0", "@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^19.0.0", "@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^19.0.0", "@angular/router": "^18.2.0",
"@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/angular-fontawesome": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@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": "^5.6.0",
"@tailwindcss/postcss": "^4.0.3", "@tailwindcss/postcss": "^4.0.3",
"ajv": "8.17.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": "^16.0.1",
"keycloak-angular": "^19.0.0", "keycloak-js": "^25.0.5",
"keycloak-js": "^26.0.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"rxjs": "~7.8.2", "rxjs": "~7.8.0",
"tailwindcss": "^4.0.3", "tailwindcss": "^4.0.3",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^19.0.0", "@angular-devkit/build-angular": "^18.2.2",
"@angular/cli": "^19.2.5", "@angular/cli": "^18.2.2",
"@angular/compiler-cli": "^19.0.0", "@angular/compiler-cli": "^18.2.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"angular-eslint": "19.3.0", "angular-eslint": "19.2.1",
"eslint": "^9.20.0", "eslint": "^9.20.0",
"jasmine-core": "~5.6.0", "jasmine-core": "~5.6.0",
"karma": "~6.4.0", "karma": "~6.4.0",
@ -54,7 +50,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"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.5.0",
"typescript-eslint": "8.29.1" "typescript-eslint": "8.26.1"
} }
} }

View file

@ -1,12 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } 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, FooterComponent], imports: [CommonModule, RouterOutlet, KeycloakAngularModule, FooterComponent],
providers: [], providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',

View file

@ -1,24 +1,59 @@
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core'; import {
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 { provideHttpClient, withInterceptors } from '@angular/common/http'; import {
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';
import { httpInterceptor } from './shared/interceptor/http.interceptor'; export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
keycloak.init({
config: {
url: 'http://localhost:9090',
realm: 'LF12',
clientId: 'lf12',
},
loadUserProfileAtStartUp: true,
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
checkLoginIframe: false,
redirectUri: window.location.origin + '/',
},
});
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
return () => initializeKeycloak(keycloak)();
}
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideRouter(routes), provideRouter(routes),
KeycloakAngularModule,
FontAwesomeModule, FontAwesomeModule,
provideHttpClient(withInterceptors([httpInterceptor])),
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
provideOAuthClient(),
{ {
provide: OAuthStorage, provide: APP_INITIALIZER,
useFactory: () => localStorage, useFactory: initializeApp,
multi: true,
deps: [KeycloakService],
}, },
KeycloakService,
provideHttpClient(withInterceptorsFromDi()),
provideExperimentalZonelessChangeDetection(),
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
},
provideAnimationsAsync(),
], ],
}; };

View file

@ -8,7 +8,7 @@ export const routes: Routes = [
component: LandingComponent, component: LandingComponent,
}, },
{ {
path: 'auth/callback', path: 'login/success',
loadComponent: () => import('./feature/login-success/login-success.component'), loadComponent: () => import('./feature/login-success/login-success.component'),
}, },
{ {

View file

@ -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 { AuthService } from './service/auth.service'; import { KeycloakService } from 'keycloak-angular';
export const authGuard: CanActivateFn = async () => { export const authGuard: CanActivateFn = async () => {
const authService = inject(AuthService); const keycloakService = inject(KeycloakService);
const router = inject(Router); const router = inject(Router);
if (authService.isLoggedIn()) { if (keycloakService.isLoggedIn()) {
return true; return true;
} }

View file

@ -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 { CommonModule } from '@angular/common'; import { NgIf } 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, CommonModule], imports: [ReactiveFormsModule, NgIf],
templateUrl: './deposit.component.html', templateUrl: './deposit.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View file

@ -6,6 +6,19 @@
<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()"
@ -31,16 +44,10 @@
</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>

View file

@ -14,7 +14,6 @@ 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',
@ -28,7 +27,6 @@ import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.
GameControlsComponent, GameControlsComponent,
GameInfoComponent, GameInfoComponent,
GameResultComponent, GameResultComponent,
DebtDialogComponent,
], ],
templateUrl: './blackjack.component.html', templateUrl: './blackjack.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -48,9 +46,7 @@ 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) => {
@ -95,6 +91,7 @@ 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) => {
@ -113,6 +110,7 @@ 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) => {
@ -139,6 +137,7 @@ 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) => {
@ -163,16 +162,12 @@ 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.getCurrentUser().subscribe((user) => { this.userService.refreshCurrentUser();
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) => {
@ -189,10 +184,6 @@ 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') {

View file

@ -1,83 +0,0 @@
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;
},
});
}
}
}

View file

@ -2,7 +2,6 @@ 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',
@ -10,15 +9,7 @@ import { GameControlsService } from '@blackjack/services/game-controls.service';
imports: [CommonModule, PlayingCardComponent], imports: [CommonModule, PlayingCardComponent],
template: ` template: `
<div class="space-y-4"> <div class="space-y-4">
<div class="flex justify-between items-center"> <h3 class="section-heading text-2xl mb-4">Croupier's Karten</h3>
<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) {
@ -47,8 +38,6 @@ 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();

View file

@ -28,27 +28,54 @@ 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>Ziehen</span> <span [class.invisible]="isActionInProgress">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>Halten</span> <span [class.invisible]="isActionInProgress">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]="!canDoubleDown || isActionInProgress" [disabled]="
gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress
"
[class.opacity-50]="isActionInProgress"
> >
<span>Verdoppeln</span> <span [class.invisible]="isActionInProgress">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>
@ -70,12 +97,4 @@ 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
);
}
} }

View file

@ -11,12 +11,11 @@ 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, AnimatedNumberComponent], imports: [CommonModule, CurrencyPipe, ReactiveFormsModule],
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>
@ -24,7 +23,7 @@ import { AnimatedNumberComponent } from '../animated-number/animated-number.comp
<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'">
<app-animated-number [value]="currentBet" [duration]="0.5"></app-animated-number> {{ currentBet | currency: 'EUR' }}
</span> </span>
</div> </div>

View file

@ -2,12 +2,11 @@ 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, AnimatedNumberComponent], imports: [CommonModule, CurrencyPipe],
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]>
@ -19,9 +18,7 @@ import { AnimatedNumberComponent } from '../animated-number/animated-number.comp
> >
<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"> <div class="font-medium text-right">{{ amount }} </div>
<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:' }}
@ -34,22 +31,21 @@ import { AnimatedNumberComponent } from '../animated-number/animated-number.comp
'text-yellow-400': isDraw, 'text-yellow-400': isDraw,
}" }"
> >
{{ isLoss ? '-' : '+' }} {{ isLoss ? '-' : '+' }}{{ amount }}
<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">
Kontostand: Gesamt:
</div> </div>
<div class="font-medium text-right border-t border-text-secondary/20 pt-3"> <div
<app-animated-number [value]="balance" [duration]="0.5"></app-animated-number> class="font-medium text-right border-t border-text-secondary/20 pt-3"
[ngClass]="{
'text-emerald': isWin,
'text-accent-red': isLoss,
'text-white': isDraw,
}"
>
{{ isWin ? '+' : isLoss ? '-' : '' }}{{ amount }}
</div> </div>
</div> </div>
</div> </div>
@ -80,7 +76,6 @@ import { AnimatedNumberComponent } from '../animated-number/animated-number.comp
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;

View file

@ -2,7 +2,6 @@ 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',
@ -10,15 +9,7 @@ import { GameControlsService } from '@blackjack/services/game-controls.service';
imports: [CommonModule, PlayingCardComponent], imports: [CommonModule, PlayingCardComponent],
template: ` template: `
<div class="space-y-4"> <div class="space-y-4">
<div class="flex justify-between items-center"> <h3 class="section-heading text-2xl mb-4">Deine Karten</h3>
<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"
@ -49,8 +40,6 @@ 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();

View file

@ -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 { AuthService } from '../../service/auth.service'; import { UserService } from '@service/user.service';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({ @Component({
selector: 'app-login-success', selector: 'app-login-success',
@ -12,32 +12,15 @@ import { OAuthService } from 'angular-oauth2-oidc';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export default class LoginSuccessComponent implements OnInit { export default class LoginSuccessComponent implements OnInit {
private authService: AuthService = inject(AuthService); private userService: UserService = inject(UserService);
private oauthService: OAuthService = inject(OAuthService); private keycloakService: KeycloakService = inject(KeycloakService);
private router: Router = inject(Router); private router: Router = inject(Router);
async ngOnInit() { async ngOnInit() {
try { const userProfile = await this.keycloakService.loadUserProfile();
if (this.oauthService.hasValidAccessToken()) { const user = await this.userService.getOrCreateUser(userProfile);
this.router.navigate(['/home']); sessionStorage.setItem('user', JSON.stringify(user));
} else {
setTimeout(() => { this.router.navigate(['home']);
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);
}
} }
} }

View file

@ -1,5 +1,5 @@
export interface User { export interface User {
authentikId: string; keycloakId: string;
username: string; username: string;
balance: number; balance: number;
} }

View file

@ -1,208 +0,0 @@
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;
}
}

View file

@ -1,5 +1,6 @@
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';
@ -12,6 +13,7 @@ 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();
} }
@ -36,25 +38,24 @@ 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', {
authentikId: id, keycloakId: id,
username: username, username: username,
}) })
.pipe(tap((user) => this.currentUserSubject.next(user))); .pipe(tap((user) => this.currentUserSubject.next(user)));
} }
public getOrCreateUser(profile: Record<string, unknown>): Observable<User> { public async getOrCreateUser(userProfile: KeycloakProfile) {
const info = profile['info'] as Record<string, unknown> | undefined; if (userProfile.id == null) {
const id = (info?.['sub'] as string) || (profile['sub'] as string); return;
const username =
(info?.['preferred_username'] as string) ||
(profile['preferred_username'] as string) ||
(profile['email'] as string) ||
(profile['name'] as string);
if (!id || !username) {
throw new Error('Invalid user profile data');
} }
return await this.getUser(userProfile.id)
.toPromise()
.then(async (user) => {
if (user) {
return user;
}
return this.createUser(id, username); return await this.createUser(userProfile.id ?? '', userProfile.username ?? '').toPromise();
});
} }
} }

View file

@ -1,176 +0,0 @@
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();
}
}

View file

@ -17,11 +17,8 @@
@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 [class]="balance() < 0 ? 'text-accent-red' : ''"> <span>{{ balance() | currency: 'EUR' : 'symbol' : '1.2-2' }}</span>
<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>
} }

View file

@ -7,23 +7,22 @@ import {
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { AuthService } from '../../../service/auth.service'; import { KeycloakService } from 'keycloak-angular';
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, AnimatedNumberComponent], imports: [RouterModule, CurrencyPipe],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NavbarComponent implements OnInit, OnDestroy { export class NavbarComponent implements OnInit, OnDestroy {
isMenuOpen = false; isMenuOpen = false;
private authService: AuthService = inject(AuthService); private keycloakService: KeycloakService = inject(KeycloakService);
isLoggedIn = this.authService.isLoggedIn(); isLoggedIn = this.keycloakService.isLoggedIn();
private userService = inject(UserService); private userService = inject(UserService);
private userSubscription: Subscription | undefined; private userSubscription: Subscription | undefined;
@ -43,14 +42,15 @@ export class NavbarComponent implements OnInit, OnDestroy {
login() { login() {
try { try {
this.authService.login(); const baseUrl = window.location.origin;
this.keycloakService.login({ redirectUri: `${baseUrl}/login/success` });
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Login failed:', error);
} }
} }
logout() { logout() {
this.authService.logout(); this.keycloakService.logout();
} }
toggleMenu() { toggleMenu() {

View file

@ -1,21 +0,0 @@
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);
}
};

View file

@ -1,7 +1,4 @@
export const environment = { export const environment = {
STRIPE_KEY: STRIPE_KEY:
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG', 'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
OAUTH_CLIENT_ID: 'MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm',
OAUTH_CLIENT_SECRET:
'GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5',
}; };