Merge pull request 'chore: Remove old Anhang and add some code to the docs' (!317) from docs/code into main
All checks were successful
Build docs / build-docs (push) Successful in 16s
All checks were successful
Build docs / build-docs (push) Successful in 16s
Reviewed-on: #317 Reviewed-by: Constantin Simonis <constantin@simonis.lol>
This commit is contained in:
commit
7221422d3b
10 changed files with 489 additions and 53 deletions
|
@ -35,6 +35,5 @@
|
||||||
\acro{URL}{Uniform Resource Locator}\acused{URL}
|
\acro{URL}{Uniform Resource Locator}\acused{URL}
|
||||||
\acro{VM}{Virtual Machine}
|
\acro{VM}{Virtual Machine}
|
||||||
\acro{XML}{Extensible Markup Language}
|
\acro{XML}{Extensible Markup Language}
|
||||||
\acro{API}{Application Programming Interface}
|
|
||||||
\acro{JWT}{JSON Web Token}
|
\acro{JWT}{JSON Web Token}
|
||||||
\end{acronym}
|
\end{acronym}
|
||||||
|
|
|
@ -13,66 +13,35 @@
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
||||||
\subsection{Use Case-Diagramm}
|
\subsection{Implementierungsbeispiele}
|
||||||
\label{app:UseCase}
|
\label{app:CodeSchichten}
|
||||||
\begin{figure}[htb]
|
|
||||||
\centering
|
\subsubsection{Frontend-Schicht: Angular Component}
|
||||||
\includegraphicsKeepAspectRatio{UseCase.pdf}{1.3}
|
\label{app:FrontendComponent}
|
||||||
\caption{Use Case-Diagramm}
|
\lstinputlisting[language=C, caption={Angular TypeScript Component - Coinflip Game}]{Listings/CoinflipComponent.ts}
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
||||||
\subsection{Amortisation}
|
\subsubsection{Controller-Schicht: Spring Boot REST Controller}
|
||||||
\label{app:Amortisation}
|
\label{app:ControllerSchicht}
|
||||||
Der Zeitpunkt der Amortisation wird als Schnittpunkt der beiden Geraden angegeben.
|
\lstinputlisting[language=java, caption={Spring Boot REST Controller - Coinflip}]{Listings/CoinflipController.java}
|
||||||
|
|
||||||
\begin{figure}[htb]
|
|
||||||
\centering
|
|
||||||
\includegraphicsKeepAspectRatio{amortisationgrafik.png}{1}
|
|
||||||
\caption{Grafische Darstellung der Amortisation}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
||||||
\subsection{composer.json Konfiguration für neusta-m2-intex-client}
|
\subsubsection{Service-Schicht: Business Logic}
|
||||||
\label{app:ComposerJson}
|
\label{app:ServiceSchicht}
|
||||||
\lstinputlisting[language=json, caption={Konfiguration für neusta-m2-intex-client}]{Listings/composer.json}
|
\lstinputlisting[language=java, caption={Service-Klasse mit Geschäftslogik - Coinflip}]{Listings/CoinflipService.java}
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
||||||
\subsection{Deklaration zur Anlage einer SQL Tabelle im Magento 2 Umfeld}
|
\subsubsection{Persistierung-Schicht: JPA Entity}
|
||||||
\label{app:InstallData}
|
\label{app:PersistierungSchicht}
|
||||||
\lstinputlisting[language=xml, caption={Deklaration zur Anlage einer SQL Tabelle im Magento 2 Umfeld}]{Listings/InstallData.xml}
|
\lstinputlisting[language=java, caption={JPA Entity - Benutzer}]{Listings/UserEntity.java}
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
||||||
\subsection{Klasse: Factory}
|
\subsubsection{Konfiguration: Application Properties}
|
||||||
\label{app:Factory}
|
\label{app:Konfiguration}
|
||||||
\lstinputlisting[language=php, caption={Klasse: Factory}]{Listings/Factory.php}
|
\lstinputlisting[caption={Spring Boot Anwendungskonfiguration}]{Listings/application.properties}
|
||||||
|
|
||||||
\clearpage
|
|
||||||
|
|
||||||
\subsection{Klasse: CustomerConnection}
|
|
||||||
\label{app:CustomerConnection}
|
|
||||||
\lstinputlisting[language=php, caption={Klasse: CustomerConnection}]{Listings/CustomerConnection.php}
|
|
||||||
|
|
||||||
\clearpage
|
|
||||||
|
|
||||||
\subsection{Klasse: Connection}
|
|
||||||
\label{app:Connection}
|
|
||||||
\lstinputlisting[language=php, caption={Abstrakte Klasse: Connection}]{Listings/Connection.php}
|
|
||||||
|
|
||||||
\clearpage
|
|
||||||
|
|
||||||
\subsection{Klasse: CustomerDataController}
|
|
||||||
\label{app:CustomerDataController}
|
|
||||||
\lstinputlisting[language=php, caption={Klasse: CustomerDataController}]{Listings/CustomerDataController.php}
|
|
||||||
|
|
||||||
\clearpage
|
|
||||||
|
|
||||||
\subsection{UnitTest: FactoryTest}
|
|
||||||
\label{app:UnitTest}
|
|
||||||
\lstinputlisting[language=php, caption={Unit Test der Klasse: Factory}]{Listings/UnitTest.php}
|
|
||||||
|
|
||||||
\clearpage
|
\clearpage
|
||||||
|
|
|
@ -36,4 +36,7 @@ Das Spielergebnis wird strukturiert an das Frontend übermittelt und enthält:
|
||||||
\item Gewinnstatus (gewonnen/verloren)
|
\item Gewinnstatus (gewonnen/verloren)
|
||||||
\item Auszahlungsbetrag (bei Gewinn: 2x Einsatz)
|
\item Auszahlungsbetrag (bei Gewinn: 2x Einsatz)
|
||||||
\item Geworfene Münzseite
|
\item Geworfene Münzseite
|
||||||
\end{itemize}
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsubsection{Implementierungsdetails}
|
||||||
|
Die vollständige Implementierung der Coinflip-Funktionalität umfasst verschiedene Architekturschichten: Das Angular Frontend-Component (siehe \ref{app:FrontendComponent}), den Spring Boot REST Controller (siehe \ref{app:ControllerSchicht}) und die Service-Schicht mit der Geschäftslogik (siehe \ref{app:ServiceSchicht}). Zusätzlich wird die Benutzer-Entity (siehe \ref{app:PersistierungSchicht}) für die Guthaben-Verwaltung verwendet.
|
|
@ -2,4 +2,4 @@
|
||||||
\label{sec:Deployment}
|
\label{sec:Deployment}
|
||||||
Es gibt zwei Server auf denen Instanzen der Applikation laufen.
|
Es gibt zwei Server auf denen Instanzen der Applikation laufen.
|
||||||
\subsection{\href{https://casino.simonis.lol/}{Entwicklungsserver}} Auf dem Entwicklungsserver läuft eine Instanz der Applikation, die für die Entwicklung und das Testen von neuen Features genutzt wird. Diese Instanz ist Lokal bei Constantin gehostet und wird durch einen Cloudflare-Tunnel öffentlich zugänglich gemacht.
|
\subsection{\href{https://casino.simonis.lol/}{Entwicklungsserver}} Auf dem Entwicklungsserver läuft eine Instanz der Applikation, die für die Entwicklung und das Testen von neuen Features genutzt wird. Diese Instanz ist Lokal bei Constantin gehostet und wird durch einen Cloudflare-Tunnel öffentlich zugänglich gemacht.
|
||||||
\subsection{\href{https://trustworthy.casino/}{Produktionsserver}} Auf dem Produktionsserver läuft die finale Version der Applikation, die für die Nutzer zugänglich ist. Diese Instanz ist öffentlich zugänglich und wird von den Nutzern genutzt. Diese Instanz ist auf einem gemieteten Server gehostet. Die Applikation wird durch eine Nginx Reverse-Proxy bereitgestellt, die Anfragen an die \acs{API} und das Frontend weiterleitet und SSL-Zertifikate verwaltet.
|
\subsection{\href{https://trustworthy.casino/}{Produktionsserver}} Auf dem Produktionsserver läuft die finale Version der Applikation, die für die Nutzer zugänglich ist. Diese Instanz ist öffentlich zugänglich und wird von den Nutzern genutzt. Diese Instanz ist auf einem gemieteten Server gehostet. Die Applikation wird durch eine Nginx Reverse-Proxy bereitgestellt, die Anfragen an die \acs{API} und das Frontend weiterleitet und SSL-Zertifikate verwaltet. Die Konfiguration der Anwendung erfolgt über Umgebungsvariablen und Properties-Dateien (siehe \ref{app:Konfiguration}).
|
||||||
|
|
|
@ -27,7 +27,7 @@ Services übernehmen die Kommunikation mit dem Backend und kapseln die Geschäft
|
||||||
\subsubsection{Backend-Architektur}
|
\subsubsection{Backend-Architektur}
|
||||||
Das Backend implementiert eine klassische mehrschichtige Architektur, die eine klare Trennung der Verantwortlichkeiten gewährleistet. Die Controller-Schicht stellt die REST-\acs{API}-Endpunkte bereit und behandelt \acs{HTTP}-Anfragen. Die Service-Schicht enthält die Geschäftslogik und orchestriert verschiedene Use Cases.
|
Das Backend implementiert eine klassische mehrschichtige Architektur, die eine klare Trennung der Verantwortlichkeiten gewährleistet. Die Controller-Schicht stellt die REST-\acs{API}-Endpunkte bereit und behandelt \acs{HTTP}-Anfragen. Die Service-Schicht enthält die Geschäftslogik und orchestriert verschiedene Use Cases.
|
||||||
|
|
||||||
Die Repository-Schicht abstrahiert den Datenzugriff und verwendet Spring Data JPA für die Kommunikation mit der Datenbank. Entity-Klassen repräsentieren die Domain-Modelle und bilden die Datenbankstrukturen ab.
|
Die Repository-Schicht abstrahiert den Datenzugriff und verwendet Spring Data JPA für die Kommunikation mit der Datenbank. Entity-Klassen repräsentieren die Domain-Modelle und bilden die Datenbankstrukturen ab. Eine detaillierte Darstellung der verschiedenen Architekturschichten mit konkreten Code-Beispielen findet sich im Anhang (siehe \ref{app:CodeSchichten}).
|
||||||
|
|
||||||
\subsection{Datenarchitektur}
|
\subsection{Datenarchitektur}
|
||||||
Die Datenbank folgt einem relationalen Design mit klar definierten Entitätsbeziehungen. Das Schema gliedert sich in mehrere Hauptbereiche: Der User Management Bereich verwaltet Benutzerkonten und Benutzerprofile. Spielbezogene Daten wie Spielstände, Wetten und Ergebnisse werden in separaten Tabellen gespeichert, um die Integrität der Spiellogik zu gewährleisten.
|
Die Datenbank folgt einem relationalen Design mit klar definierten Entitätsbeziehungen. Das Schema gliedert sich in mehrere Hauptbereiche: Der User Management Bereich verwaltet Benutzerkonten und Benutzerprofile. Spielbezogene Daten wie Spielstände, Wetten und Ergebnisse werden in separaten Tabellen gespeichert, um die Integrität der Spiellogik zu gewährleisten.
|
||||||
|
|
247
projektdokumentation/Listings/CoinflipComponent.ts
Normal file
247
projektdokumentation/Listings/CoinflipComponent.ts
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
import { NgClass, NgIf, CurrencyPipe, CommonModule } from '@angular/common';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
inject,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
|
||||||
|
import { catchError, finalize } from 'rxjs/operators';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { AuthService } from '@service/auth.service';
|
||||||
|
import { AudioService } from '@shared/services/audio.service';
|
||||||
|
import { CoinflipGame, CoinflipRequest } from './models/coinflip.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-coinflip',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AnimatedNumberComponent, CurrencyPipe, FormsModule, CommonModule, NgIf, NgClass],
|
||||||
|
templateUrl: './coinflip.component.html',
|
||||||
|
styleUrl: './coinflip.component.css',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export default class CoinflipComponent implements OnInit {
|
||||||
|
currentBet = signal(10);
|
||||||
|
balance = signal(0);
|
||||||
|
gameInProgress = signal(false);
|
||||||
|
isActionInProgress = signal(false);
|
||||||
|
gameResult = signal<CoinflipGame | null>(null);
|
||||||
|
betInputValue = signal(10);
|
||||||
|
errorMessage = signal('');
|
||||||
|
isInvalidBet = signal(false);
|
||||||
|
|
||||||
|
@ViewChild('coinElement') coinElement?: ElementRef;
|
||||||
|
|
||||||
|
audioService = inject(AudioService);
|
||||||
|
authService = inject(AuthService);
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
private coinflipSound?: HTMLAudioElement;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Abonniere Benutzerupdates fuer Echtzeitaktualisierungen des Guthabens
|
||||||
|
this.authService.userSubject.subscribe((user) => {
|
||||||
|
if (user) {
|
||||||
|
this.balance.set(user.balance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialisiere Muenzwurf-Sound
|
||||||
|
this.coinflipSound = new Audio('/sounds/coinflip.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
setBetAmount(percentage: number) {
|
||||||
|
const newBet = Math.floor(this.balance() * percentage);
|
||||||
|
this.betInputValue.set(newBet > 0 ? newBet : 1);
|
||||||
|
this.currentBet.set(this.betInputValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBet(event: Event) {
|
||||||
|
const inputElement = event.target as HTMLInputElement;
|
||||||
|
let value = Number(inputElement.value);
|
||||||
|
|
||||||
|
// Setze ungueltigen Einsatz-Status zurueck
|
||||||
|
this.isInvalidBet.set(false);
|
||||||
|
|
||||||
|
// Erzwinge Mindesteinsatz von 1
|
||||||
|
if (value <= 0) {
|
||||||
|
value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begrenze Einsatz auf verfuegbares Guthaben und zeige Feedback
|
||||||
|
if (value > this.balance()) {
|
||||||
|
value = this.balance();
|
||||||
|
// Visuelles Feedback anzeigen
|
||||||
|
this.isInvalidBet.set(true);
|
||||||
|
// Zeige den Fehler kurz an
|
||||||
|
setTimeout(() => this.isInvalidBet.set(false), 800);
|
||||||
|
// Aktualisiere das Eingabefeld direkt, um dem Benutzer den maximalen Wert anzuzeigen
|
||||||
|
inputElement.value = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Signale
|
||||||
|
this.betInputValue.set(value);
|
||||||
|
this.currentBet.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
betHeads() {
|
||||||
|
this.placeBet('HEAD');
|
||||||
|
}
|
||||||
|
|
||||||
|
betTails() {
|
||||||
|
this.placeBet('TAILS');
|
||||||
|
}
|
||||||
|
|
||||||
|
private placeBet(side: 'HEAD' | 'TAILS') {
|
||||||
|
if (this.gameInProgress() || this.isActionInProgress()) return;
|
||||||
|
|
||||||
|
// Setze vorheriges Ergebnis zurueck
|
||||||
|
this.gameResult.set(null);
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
// Setze Spielstatus
|
||||||
|
this.gameInProgress.set(true);
|
||||||
|
this.isActionInProgress.set(true);
|
||||||
|
|
||||||
|
// Spiele Einsatz-Sound
|
||||||
|
this.audioService.playBetSound();
|
||||||
|
|
||||||
|
// Erstelle Einsatz-Anfrage
|
||||||
|
const request: CoinflipRequest = {
|
||||||
|
betAmount: this.currentBet(),
|
||||||
|
coinSide: side,
|
||||||
|
};
|
||||||
|
|
||||||
|
// API aufrufen
|
||||||
|
this.http
|
||||||
|
.post<CoinflipGame>('/backend/coinflip', request)
|
||||||
|
.pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('Fehler beim Spielen von Coinflip:', error);
|
||||||
|
|
||||||
|
if (error.status === 400 && error.error.message.includes('insufficient')) {
|
||||||
|
this.errorMessage.set('Unzureichendes Guthaben');
|
||||||
|
} else {
|
||||||
|
this.errorMessage.set('Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gameInProgress.set(false);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
this.isActionInProgress.set(false);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((result) => {
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
console.log('API-Antwort:', result);
|
||||||
|
|
||||||
|
// Behebe moegliche Inkonsistenzen bei der Eigenschaftenbenennung vom Backend
|
||||||
|
const fixedResult: CoinflipGame = {
|
||||||
|
isWin: result.isWin ?? result.win,
|
||||||
|
payout: result.payout,
|
||||||
|
coinSide: result.coinSide,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Korrigiertes Ergebnis:', fixedResult);
|
||||||
|
|
||||||
|
// Spiele Muenzwurf-Animation und -Sound
|
||||||
|
this.playCoinFlipAnimation(fixedResult.coinSide);
|
||||||
|
|
||||||
|
// Setze Ergebnis nach Abschluss der Animation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.gameResult.set(fixedResult);
|
||||||
|
|
||||||
|
// Aktualisiere Guthaben mit neuem Wert vom Auth-Service
|
||||||
|
this.authService.loadCurrentUser();
|
||||||
|
|
||||||
|
// Spiele Gewinn-Sound, wenn der Spieler gewonnen hat
|
||||||
|
if (fixedResult.isWin) {
|
||||||
|
this.audioService.playWinSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze Spielstatus nach Anzeigen des Ergebnisses zurueck
|
||||||
|
setTimeout(() => {
|
||||||
|
this.gameInProgress.set(false);
|
||||||
|
}, 1500);
|
||||||
|
}, 1100); // Kurz nach Ende der Animation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private playCoinFlipAnimation(result: 'HEAD' | 'TAILS') {
|
||||||
|
if (!this.coinElement) return;
|
||||||
|
|
||||||
|
const coinEl = this.coinElement.nativeElement;
|
||||||
|
|
||||||
|
// Setze bestehende Animationen zurueck
|
||||||
|
coinEl.classList.remove('animate-to-heads', 'animate-to-tails');
|
||||||
|
|
||||||
|
// Setze alle Inline-Styles von vorherigen Animationen zurueck
|
||||||
|
coinEl.style.transform = '';
|
||||||
|
|
||||||
|
// Erzwinge Reflow, um Animation neu zu starten
|
||||||
|
void coinEl.offsetWidth;
|
||||||
|
|
||||||
|
// Spiele Muenzwurf-Sound
|
||||||
|
if (this.coinflipSound) {
|
||||||
|
this.coinflipSound.currentTime = 0;
|
||||||
|
this.coinflipSound
|
||||||
|
.play()
|
||||||
|
.catch((err) => console.error('Fehler beim Abspielen des Sounds:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuege passende Animationsklasse basierend auf dem Ergebnis hinzu
|
||||||
|
if (result === 'HEAD') {
|
||||||
|
coinEl.classList.add('animate-to-heads');
|
||||||
|
} else {
|
||||||
|
coinEl.classList.add('animate-to-tails');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Animation angewendet fuer Ergebnis: ${result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert Eingabe waehrend der Benutzer tippt, um ungueltige Werte zu verhindern
|
||||||
|
*/
|
||||||
|
validateBetInput(event: KeyboardEvent) {
|
||||||
|
// Erlaube Navigationstasten (Pfeile, Entf, Ruecktaste, Tab)
|
||||||
|
const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab'];
|
||||||
|
if (navigationKeys.includes(event.key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erlaube nur Zahlen
|
||||||
|
if (!/^\d$/.test(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ermittle den Wert, der nach dem Tastendruck entstehen wuerde
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const currentValue = input.value;
|
||||||
|
const cursorPosition = input.selectionStart || 0;
|
||||||
|
const newValue =
|
||||||
|
currentValue.substring(0, cursorPosition) +
|
||||||
|
event.key +
|
||||||
|
currentValue.substring(input.selectionEnd || cursorPosition);
|
||||||
|
const numValue = Number(newValue);
|
||||||
|
|
||||||
|
// Verhindere Werte, die groesser als das Guthaben sind
|
||||||
|
if (numValue > this.balance()) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultClass() {
|
||||||
|
if (!this.gameResult()) return '';
|
||||||
|
const result = this.gameResult();
|
||||||
|
const isWinner = result?.isWin || result?.win;
|
||||||
|
return isWinner ? 'text-emerald-500' : 'text-accent-red';
|
||||||
|
}
|
||||||
|
}
|
38
projektdokumentation/Listings/CoinflipController.java
Normal file
38
projektdokumentation/Listings/CoinflipController.java
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package de.szut.casino.coinflip;
|
||||||
|
|
||||||
|
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
||||||
|
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
||||||
|
import de.szut.casino.shared.service.BalanceService;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import de.szut.casino.user.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class CoinflipController {
|
||||||
|
private final UserService userService;
|
||||||
|
private final BalanceService balanceService;
|
||||||
|
private final CoinflipService coinflipService;
|
||||||
|
|
||||||
|
public CoinflipController(UserService userService, BalanceService balanceService, CoinflipService coinflipService) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.balanceService = balanceService;
|
||||||
|
this.coinflipService = coinflipService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/coinflip")
|
||||||
|
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
|
||||||
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
|
||||||
|
if (!this.balanceService.hasFunds(user, coinflipDto)) {
|
||||||
|
throw new InsufficientFundsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(coinflipService.play(user, coinflipDto));
|
||||||
|
}
|
||||||
|
}
|
35
projektdokumentation/Listings/CoinflipService.java
Normal file
35
projektdokumentation/Listings/CoinflipService.java
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package de.szut.casino.coinflip;
|
||||||
|
|
||||||
|
import de.szut.casino.shared.service.BalanceService;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CoinflipService {
|
||||||
|
private final Random random;
|
||||||
|
private final BalanceService balanceService;
|
||||||
|
|
||||||
|
public CoinflipService(BalanceService balanceService, Random random) {
|
||||||
|
this.balanceService = balanceService;
|
||||||
|
this.random = random;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
|
||||||
|
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
|
||||||
|
|
||||||
|
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
|
||||||
|
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO, coinSide);
|
||||||
|
if (coinSide == coinflipDto.getCoinSide()) {
|
||||||
|
coinflipResult.setWin(true);
|
||||||
|
|
||||||
|
BigDecimal payout = coinflipDto.getBetAmount().multiply(BigDecimal.TWO);
|
||||||
|
this.balanceService.addFunds(user, payout);
|
||||||
|
coinflipResult.setPayout(payout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coinflipResult;
|
||||||
|
}
|
||||||
|
}
|
92
projektdokumentation/Listings/UserEntity.java
Normal file
92
projektdokumentation/Listings/UserEntity.java
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package de.szut.casino.user;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class UserEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(precision = 19, scale = 2)
|
||||||
|
private BigDecimal balance;
|
||||||
|
|
||||||
|
private Boolean emailVerified = false;
|
||||||
|
|
||||||
|
private String verificationToken;
|
||||||
|
|
||||||
|
private String passwordResetToken;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private AuthProvider provider = AuthProvider.LOCAL;
|
||||||
|
|
||||||
|
private String providerId;
|
||||||
|
|
||||||
|
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
|
||||||
|
this.email = email;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.balance = balance;
|
||||||
|
this.verificationToken = verificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {
|
||||||
|
this.email = email;
|
||||||
|
this.username = username;
|
||||||
|
this.provider = provider;
|
||||||
|
this.providerId = providerId;
|
||||||
|
this.balance = balance;
|
||||||
|
this.emailVerified = true; // OAuth providers verify emails
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBalance(BigDecimal amountToAdd) {
|
||||||
|
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.balance == null) {
|
||||||
|
this.balance = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.balance = this.balance.add(amountToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void subtractBalance(BigDecimal amountToSubtract) {
|
||||||
|
if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new IllegalArgumentException("Amount to subtract must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.balance == null) {
|
||||||
|
this.balance = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.balance.compareTo(amountToSubtract) < 0) {
|
||||||
|
throw new IllegalStateException("Insufficient funds to subtract " + amountToSubtract);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.balance = this.balance.subtract(amountToSubtract);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailAddress() {
|
||||||
|
return "${name} <${email}>".replace("${name}", this.username).replace("${email}", this.email);
|
||||||
|
}
|
||||||
|
}
|
53
projektdokumentation/Listings/application.properties
Normal file
53
projektdokumentation/Listings/application.properties
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:postgresdb}
|
||||||
|
spring.datasource.username=${DB_USER:postgres_user}
|
||||||
|
spring.datasource.password=${DB_PASS:postgres_pass}
|
||||||
|
server.port=${HTTP_PORT:8080}
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
||||||
|
stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
|
||||||
|
|
||||||
|
app.frontend-host=${FE_URL:http://localhost:4200}
|
||||||
|
|
||||||
|
app.mail.authentication=${MAIL_AUTHENTICATION:false}
|
||||||
|
app.mail.host=${MAIL_HOST:localhost}
|
||||||
|
app.mail.port=${MAIL_PORT:1025}
|
||||||
|
app.mail.username=${MAIL_USER:null}
|
||||||
|
app.mail.password=${MAIL_PASS:null}
|
||||||
|
app.mail.from-address=${MAIL_FROM:casino@localhost}
|
||||||
|
app.mail.protocol=${MAIL_PROTOCOL:smtp}
|
||||||
|
|
||||||
|
spring.application.name=casino
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
|
||||||
|
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.level.org.springframework.security=DEBUG
|
||||||
|
|
||||||
|
# Swagger
|
||||||
|
springdoc.swagger-ui.path=swagger
|
||||||
|
springdoc.swagger-ui.try-it-out-enabled=true
|
||||||
|
|
||||||
|
# GitHub OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
|
||||||
|
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
|
||||||
|
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
|
||||||
|
spring.security.oauth2.client.registration.github.scope=user:email,read:user
|
||||||
|
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
|
||||||
|
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
|
||||||
|
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
|
||||||
|
spring.security.oauth2.client.provider.github.user-name-attribute=login
|
||||||
|
|
||||||
|
# OAuth Success and Failure URLs
|
||||||
|
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
|
||||||
|
|
||||||
|
# Google OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
|
||||||
|
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
|
||||||
|
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
|
||||||
|
spring.security.oauth2.client.registration.google.scope=email,profile
|
||||||
|
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
|
||||||
|
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||||
|
spring.security.oauth2.client.provider.google.user-name-attribute=sub
|
Reference in a new issue