diff --git a/backend/requests/webhook.http b/backend/requests/webhook.http new file mode 100644 index 0000000..8fa7f26 --- /dev/null +++ b/backend/requests/webhook.http @@ -0,0 +1 @@ +POST localhost:8080/webhook diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java index 6bc0fbc..b2918cf 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -6,10 +6,20 @@ import com.stripe.model.checkout.Session; import com.stripe.param.checkout.SessionCreateParams; import de.szut.casino.deposit.dto.AmountDto; import de.szut.casino.deposit.dto.SessionIdDto; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import de.szut.casino.user.UserService; +import de.szut.casino.user.dto.KeycloakUserDto; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; @RestController public class DepositController { @@ -20,10 +30,26 @@ public class DepositController { @Value("${app.frontend-host}") private String frontendHost; + private final TransactionService transactionService; + + private final RestTemplate restTemplate; + + private final UserRepository userRepository; + + + public DepositController(TransactionService transactionService, RestTemplate restTemplate, UserRepository userRepository) { + this.transactionService = transactionService; + this.restTemplate = restTemplate; + this.userRepository = userRepository; + } + @PostMapping("/deposit/checkout") - public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto) throws StripeException { + public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException { Stripe.apiKey = stripeKey; + KeycloakUserDto userData = getKeycloakUserInfo(token); + Optional optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub()); + SessionCreateParams params = SessionCreateParams.builder() .addLineItem(SessionCreateParams.LineItem.builder() .setAmount((long) amountDto.getAmount() * 100) @@ -38,7 +64,21 @@ public class DepositController { Session session = Session.create(params); + if (optionalUserEntity.isEmpty()) { + throw new RuntimeException("User doesnt exist"); + } + + transactionService.createTransaction(optionalUserEntity.get(), session.getId(), amountDto.getAmount()); + return ResponseEntity.ok(new SessionIdDto(session.getId())); } + + private KeycloakUserDto getKeycloakUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + + return response.getBody(); + } } diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java new file mode 100644 index 0000000..e3fc412 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java @@ -0,0 +1,29 @@ +package de.szut.casino.deposit; + +import de.szut.casino.user.UserEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Setter +@Getter +@Entity +public class TransactionEntity { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; + + @Column(unique = true) + private String sessionId = null; + + private double amount = 0; + + @Enumerated(EnumType.STRING) + private TransactionStatus status = TransactionStatus.PROCESSING; +} diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java new file mode 100644 index 0000000..94b9b6b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java @@ -0,0 +1,14 @@ +package de.szut.casino.deposit; + +import de.szut.casino.user.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public interface TransactionRepository extends JpaRepository { + @Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1") + Optional findOneBySessionID(String sessionId); +} diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java new file mode 100644 index 0000000..24d4382 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java @@ -0,0 +1,62 @@ +package de.szut.casino.deposit; + +import com.stripe.exception.StripeException; +import com.stripe.model.checkout.Session; +import com.stripe.param.checkout.SessionRetrieveParams; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.Objects; +import java.util.Optional; + +@Service +public class TransactionService { + private final TransactionRepository transactionRepository; + private final UserRepository userRepository; + + public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository) { + this.transactionRepository = transactionRepository; + this.userRepository = userRepository; + } + + public void createTransaction( + UserEntity user, + String sessionID, + Double amount + ) { + TransactionEntity transaction = new TransactionEntity(); + + transaction.setUser(user); + transaction.setSessionId(sessionID); + transaction.setAmount(amount); + + transactionRepository.save(transaction); + } + + public void fulfillCheckout(String sessionID) throws StripeException { + SessionRetrieveParams params = SessionRetrieveParams.builder() + .addExpand("line_items") + .build(); + Session checkoutSession = Session.retrieve(sessionID, params, null); + + if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) { + return; + } + + Optional optionalTransaction = transactionRepository.findOneBySessionID(sessionID); + if (optionalTransaction.isEmpty()) { + throw new RuntimeException("Transaction not found"); + } + + TransactionEntity transaction = optionalTransaction.get(); + transaction.setStatus(TransactionStatus.SUCCEEDED); + + UserEntity user = transaction.getUser(); + user.addBalance(checkoutSession.getAmountTotal()); + + userRepository.save(user); + transactionRepository.save(transaction); + } + +} diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java b/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java new file mode 100644 index 0000000..93ae142 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java @@ -0,0 +1,6 @@ +package de.szut.casino.deposit; + +public enum TransactionStatus { + PROCESSING, + SUCCEEDED, +} diff --git a/backend/src/main/java/de/szut/casino/deposit/WebhookController.java b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java new file mode 100644 index 0000000..3bcf699 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java @@ -0,0 +1,64 @@ +package de.szut.casino.deposit; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import com.stripe.Stripe; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.stripe.model.*; +import com.stripe.model.checkout.Session; +import com.stripe.net.Webhook; +import com.stripe.param.checkout.SessionRetrieveParams; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Objects; +import java.util.Optional; + +@RestController +public class WebhookController { + private static final Logger logger = LoggerFactory.getLogger(WebhookController.class); + @Value("${stripe.secret.key}") + private String stripeSecretKey; + + @Value("${stripe.webhook.secret}") + private String webhookSecret; + + private final TransactionService transactionService; + + public WebhookController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @PostConstruct + public void init() { + Stripe.apiKey = stripeSecretKey; + } + + @PostMapping("/webhook") + public ResponseEntity webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException { + Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret); + + System.out.println(event.getType()); + + switch (event.getType()) { + case "checkout.session.completed": + case "checkout.session.async_payment_succeeded": + Session session = (Session) event.getData().getObject(); + + this.transactionService.fulfillCheckout(session.getId()); + break; + default: + + } + + return ResponseEntity.ok().body(null); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java b/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java index 6be8b83..48c04aa 100644 --- a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java +++ b/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java @@ -2,6 +2,7 @@ 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; @@ -47,12 +48,16 @@ class KeycloakSecurityConfig { @Bean public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll() - .anyRequest().authenticated() - ) - .oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults())); - + 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(); } diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 83d8f44..c0cc92f 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -7,6 +7,7 @@ import jakarta.persistence.Id; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; + import java.math.BigDecimal; @Setter @@ -29,4 +30,14 @@ public class UserEntity { this.username = username; this.balance = balance; } + + public void addBalance(long amountInCents) { + BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2); + + if (this.balance == null) { + this.balance = amountToAdd; + } else { + this.balance = this.balance.add(amountToAdd); + } + } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index c7ccadf..00b9f86 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -3,7 +3,8 @@ spring.datasource.username=postgres_user spring.datasource.password=postgres_pass server.port=8080 spring.jpa.hibernate.ddl-auto=create-drop -stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I} +stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51PI7FRJX3NPIs0QEHQfzDLhEVoRluIhKw2CfpH2OCUOQI8oNrAKzgHy4eTvdoMdB0Tj5RV3cfSMoifFWy1EWySxO00JViZ0kl2} +stripe.webhook.secret=whsec_600e1e31dc04d7b0510b904dd352cadbc84dc73659730709ee81997657397b91 app.frontend-host=http://localhost:4200 spring.application.name=lf12_starter diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 53866cc..c27aa44 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { STRIPE_KEY: - 'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG', + 'pk_test_51PI7FRJX3NPIs0QEHnKXgCHTUnCp72omcZhOB4VXPvMOUlj1bsldptRTocGzcQ9WNWxNVfCKJoNbNDWM0peUQLLW007VFLlXAI', };