Compare commits

..

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

170 changed files with 1572 additions and 7508 deletions

View file

@ -1,20 +0,0 @@
frontend:
- changed-files:
- any-glob-to-any-file:
- "frontend/**"
backend:
- changed-files:
- any-glob-to-any-file:
- "backend/**"
ci:
- changed-files:
- any-glob-to-any-file:
- ".gitea/**"
docs:
- changed-files:
- any-glob-to-any-file:
- "projektdokumentation/**"

View file

@ -1,19 +0,0 @@
buckets:
- maxSize: 80
label: size/small
comment: null
- maxSize: 200
label: size/medium
comment: null
- maxSize: 2000
label: size/large
comment: >
👮‍♀️⚠️ This is a friendly reminder that the diff size of this PR is bigger than
200 lines we aim for. Please consider splitting this PR into more digestible pieces!
- maxSize: Infinity
label: size/huge
comment: >
👮‍♀️🛑 This PR's diff size is quite huge.
Hopefully you know what you're doing.
If you did not commit a lot of autogenerated files intentionally,
there are few good reasons for this.

View file

@ -29,32 +29,6 @@ jobs:
workflow:
- '.gitea/workflows/**'
test-backend:
runs-on: ubuntu-latest
name: "Backend Tests"
needs: changed_files
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: "cimg/openjdk:23.0-node"
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Run tests"
working-directory: ./backend
run: |
./gradlew test
- name: "Cache checkstyle results"
uses: actions/upload-artifact@v4
with:
name: checkstyle-results
path: backend/build/reports/checkstyle
- name: "Stop Gradle"
working-directory: ./backend
run: ./gradlew --stop
checkstyle:
runs-on: ubuntu-latest
name: "Checkstyle Main"
@ -165,46 +139,6 @@ jobs:
cd frontend
bun run lint
playwright:
runs-on: ubuntu-latest
name: Playwright
needs: changed_files
container:
image: git.kjan.de/actions/runner-casino-playwright:latest
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin" # See 'Supported distributions' for available options
java-version: "23"
- name: Install bun
uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
working-directory: ./frontend
with:
path: |
frontend/node_modules/
key: ${{ runner.os }}-bun-
restore-keys: |
${{ runner.os }}-bun-
- run: bun add -g concurrently
- name: Install dependencies
run: |
cd frontend
bun install
- uses: actions/setup-node@v4
with:
node-version: 22.12
working-directory: ./frontend
- name: Run Playwright tests
env:
CI: true
SPRING_PROFILES_ACTIVE: inmemory
working-directory: ./frontend
run: bash -c "source $HOME/.cargo/env && bunx playwright test"
oxlint:
runs-on: docker
name: oxlint

View file

@ -1,124 +0,0 @@
name: Claude Gitea PR Interaction via Comment
on:
issue_comment:
types: [created]
jobs:
claude-interact-on-comment:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '@Claude')
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git diff against main/master
- name: Set Tea Version
id: tea_version
run: echo "version=0.9.2" >> $GITHUB_OUTPUT # Check for the latest stable version
- name: Download Tea CLI
run: |
TEA_VERSION=$(echo "${{ steps.tea_version.outputs.version }}")
wget "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -O tea
chmod +x tea
sudo mv tea /usr/local/bin/tea
- name: Verify Tea Installation
run: tea --version
- name: Add Gitea Login
env:
GITEA_URL: ${{ secrets._GITEA_URL }}
GITEA_TOKEN: ${{ secrets._GITEA_TOKEN }}
run: |
if [ -z "$GITEA_URL" ]; then
echo "Error: GITEA_URL secret is not set."
exit 1
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Error: GITEA_TOKEN secret is not set."
exit 1
fi
INSECURE_FLAG=""
if [[ "${GITEA_URL}" == http://* ]]; then
INSECURE_FLAG="--insecure"
fi
tea login add --name mygitea --url "$GITEA_URL" --token "$GITEA_TOKEN" $INSECURE_FLAG
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Install claude-code
run: bun i -g @anthropic-ai/claude-code
- name: Claude Process PR Comment
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
GITEA_URL: ${{ secrets._GITEA_URL }}
run: |
claude --allowedTools "Bash(tea:*)" --allowedTools "Bash(git:*)" --allowedTools "Read" --allowedTools "Grep" --allowedTools "WebFetch" --allowedTools "Glob" --allowedTools "LS" -p "You are an AI assistant integrated with Gitea (at ${GITEA_URL}) via the 'tea' CLI.
You have been invoked because user '${COMMENT_AUTHOR}' left the following comment on Pull Request #${PR_NUMBER}:
---
${COMMENT_BODY}
---
Your primary task is to:
1. Carefully understand the user's request within their comment.
2. Use the 'tea' CLI to perform the requested action(s) on Pull Request #${PR_NUMBER}.
3. If the request is to review the PR, fetch the diff against the PR's base branch (e.g., 'git fetch origin main && git diff origin/main...HEAD' or similar; adapt branch name if necessary, or use 'tea pr diff ${PR_NUMBER}') and provide constructive feedback.
4. For other actions, translate the user's intent into the appropriate 'tea' command.
**How to Post Reviews and Other Feedback:**
- When you provide a review, post it as a comment using:
\`tea pr comment ${PR_NUMBER} \"Claude's Review:\n[Your detailed review, mentioning files and line numbers.]\"\`
- For other informational responses or clarifications, also use \`tea pr comment ...\`.
**Critical: Handling Approval, Rejection, or Merge Requests:**
Pull Request approval, rejection, and merging are critical actions and should not be used to 'cheat' the review process. You cannot verify Gitea user permissions.
- If a user comments asking you to directly approve (e.g., '@claude approve this'), merge, or reject a PR:
1. **Do NOT blindly execute these commands.**
2. **Approval/Merge:**
- State in a comment (using \`tea pr comment ...\`) that direct approval/merge requests via you are typically for convenience after a proper human review process has been implicitly completed or if the requester is a designated maintainer explicitly overriding.
- If the PR has not been reviewed by you yet, and the comment implies a review is also needed, perform the review FIRST and post it.
- You should only consider proceeding with a \`tea pr approve ${PR_NUMBER}\` or \`tea pr merge ${PR_NUMBER}\` command if:
a. The comment explicitly states that all necessary human reviews are complete and this is just a formal step by a trusted user.
b. OR, your own comprehensive review found no critical issues and the request seems appropriate in context.
- If in doubt, default to posting your review (if applicable) and stating that a maintainer should perform the final approval/merge. Your goal is to assist, not to bypass established review procedures.
3. **Rejection/Requesting Changes:**
- If asked to reject or request changes, you should typically base this on your own review of the PR's changes.
- First, perform a review if you haven't already.
- Then, you can use \`tea pr reject ${PR_NUMBER} \"Claude's Review Summary: [summary of reasons for rejection/changes based on your review]\"\`. Ensure your detailed review is also available as a comment.
Examples of interpreting comments and generating appropriate \`tea\` commands (keeping the above critical guidelines in mind):
- User: '@claude LGTM, approve this' -> You: First, consider if a review is implied or done. If so, and you agree, you might generate \`tea pr approve ${PR_NUMBER}\`. If not, you might generate \`tea pr comment ${PR_NUMBER} \"Claude: I can approve this if the standard review process is complete. Have maintainers reviewed this?\"\` or perform your own review and then comment.
- User: '@claude please review this PR' -> You: Get diffs, review, then generate \`tea pr comment ${PR_NUMBER} \"Claude's Review: ...\"\`.
- User: '@claude close this PR' -> You: Generate \`tea pr close ${PR_NUMBER}\` and optionally \`tea pr comment ${PR_NUMBER} \"Claude: PR #${PR_NUMBER} has been closed as requested.\"\`.
- User: '@claude add label enhancement' -> You: Generate \`tea pr label ${PR_NUMBER} --add enhancement\` and \`tea pr comment ${PR_NUMBER} \"Claude: Label 'enhancement' added to PR #${PR_NUMBER}.\"\`
- User: '@claude what are the labels on this PR?' -> You: Generate \`tea pr label ${PR_NUMBER} --list\` (this command outputs to stdout, which is fine for your internal use). Then, to inform the user, you generate: \`tea pr comment ${PR_NUMBER} \"Claude: The current labels are: [output from tea pr label --list].\"\` (You'll need to capture the output of the first command to formulate the second if the tool allows such chaining, otherwise, focus on commands that directly achieve the user's goal or report information). *Self-correction: The Bash tool can capture output. So, if you need to run a \`tea\` command to get information for yourself, do so, then use that information to formulate your \`tea pr comment ...\` to the user.*
**IMPORTANT GUIDELINES FOR YOUR OPERATION AND RESPONSE GENERATION:**
- **Your SOLE METHOD of communicating back to the user on Gitea is by generating a \`tea pr comment ${PR_NUMBER} \"...\"\` command.** This is non-negotiable. Do not output plain text messages intended for the user. Your response *is* the command.
- **Use the 'tea' CLI for ALL Gitea interactions.** This includes fetching PR details, diffs, labels, status, and posting comments, reviews, approvals, etc.
- **For PR reviews, ALWAYS analyze the diff.** Use \`tea pr diff ${PR_NUMBER}\` or git commands to get the diff. Make sure to mention specific files and line numbers in your review comment.
- **Be precise with 'tea' commands.** If a user's request is ambiguous, DO NOT GUESS. Instead, generate a \`tea pr comment ${PR_NUMBER} \"Claude Asks: [Your clarifying question]\"\` command to ask for more details.
- **Execute only necessary 'tea' command(s).** If a user asks for a review, your primary output should be the \`tea pr comment ...\` command containing the review. If they ask to add a label, your output should be \`tea pr label ...\` and then a confirmation \`tea pr comment ...\`.
- **Ensure reviews are professional, constructive, and helpful.**
- **If you need to perform an action AND then report on it, generate both \`tea\` commands sequentially.** For example, to add a label and confirm:
\`tea pr label ${PR_NUMBER} --add bug\`
\`tea pr comment ${PR_NUMBER} "Claude: I've added the 'bug' label."\`
The GitHub Actions workflow will execute these commands one after another.
- **If a user's request cannot be fulfilled using the 'tea' CLI or the allowed tools, explain this limitation by generating a \`tea pr comment ...\` command.** For example: \`tea pr comment ${PR_NUMBER} "Claude: I cannot perform that action as it's outside my current capabilities or allowed tools."\`
- **Think step-by-step.** 1. Understand request. 2. Identify necessary `tea` command(s). 3. If it's a review, get the diff. 4. Formulate the `tea` command(s) as your direct output.
**Final Check before outputting:**
"Is my entire response that's intended for the Gitea user wrapped in a \`tea pr comment ${PR_NUMBER} '...' \` command (or another appropriate \`tea\` command if it's an action like \`tea pr label ...\`)? If not, I must fix it."
You are now ready to process the comment. Remember, your output will be executed in a shell. Generate only the \`tea\` command(s) needed.
"

View file

@ -1,16 +0,0 @@
name: Claude PR Review
on:
pull_request:
types: [opened, synchronize] # Runs on new PRs and updates
jobs:
claude-code:
runs-on: ubuntu-latest
steps:
- name: Claude
uses: https://git.kjan.de/actions/claude-pr-review@v1.0.4
with:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITEA_URL: ${{ secrets._GITEA_URL }}
GITEA_CLAUDE_TOKEN: ${{ secrets._GITEA_TOKEN }}

View file

@ -1,29 +0,0 @@
name: Build docs
on:
pull_request:
push:
branches: [main]
jobs:
build-docs:
runs-on: ubuntu-latest
container:
image: git.kjan.de/actions/runner-latex:latest
env:
# Edit here with the names of your latex file and directory (can use ".")
DIR: projektdokumentation
FILE: Projektdokumentation.tex
steps:
- name: Checkout
uses: actions/checkout@v4
- name: LaTeX compile
working-directory: ${{ env.DIR }}
run: latexmk -pdf ${{ env.FILE }}
- name: Upload artifacts
uses: https://git.kjan.de/actions/upload-artifact@v3 # Do not upgrade
with:
name: Doku
path: projektdokumentation/Projektdokumentation.pdf

View file

@ -1,14 +0,0 @@
name: "Pull Request Labeler"
on:
pull_request_target:
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
configuration-path: ".gitea/labeler.yml"

View file

@ -1,17 +0,0 @@
name: Label PRs based on size
on: [pull_request]
jobs:
add_pr_size_label:
runs-on: ubuntu-latest
name: Check PR size
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Label and comment PR
uses: boschresearch/pr-size-labeler@v5.0.1
with:
bucketConfigFile: ".gitea/size.yml"

View file

@ -1,15 +0,0 @@
name: "Close stale issues and PRs"
on:
workflow_dispatch:
schedule:
- cron: "@hourly"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-pr-message: "Will be closed in x days bc yo mom is a bitch. im not telling you when it will be closed fuckface"
days-before-pr-stale: 2
days-before-pr-close: 3

View file

@ -15,113 +15,96 @@ Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#
## Tech Stack
### Frontend
- Angular 20
- Angular 19
- TailwindCSS
- Keycloak integration
- Stripe payment integration
### Backend
- Spring Boot (Java)
- PostgreSQL database
- Keycloak for authentication/authorization
- Stripe API for payment processing
### Infrastructure
- Docker containerization for all services
## Getting Started
### Prerequisites
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
- Java JDK 17+
- Node.js 18+
* [Docker](https://docs.docker.com/get-docker/)
* [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
* Java JDK 17+
* Node.js 18+
### Setting Up the Environment
1. Clone the repository
```bash
git clone <repository-url>
cd casino
```
2. Start the Docker services
```bash
cd docker
docker-compose up -d
```
This will start:
- PostgreSQL database
- Keycloak authentication server
### Running the Backend
1. Navigate to the backend directory
```bash
cd backend
```
2. Start the Spring Boot application
```bash
./gradlew bootRun
```
You may optionally install [watchexec](https://github.com/watchexec/watchexec?tab=readme-ov-file) and use this command to autorecompile the backend on file changes:
```bash
watchexec -r -e java ./gradlew :bootRun
```
The backend will be available at:
- API endpoint: <http://localhost:8080>
- Swagger documentation: <http://localhost:8080/swagger>
- API endpoint: http://localhost:8080
- Swagger documentation: http://localhost:8080/swagger
### Running the Frontend
1. Navigate to the frontend directory
```bash
cd frontend
```
2. Install dependencies
```bash
npm install
```
3. Start the development server
```bash
npm run dev
```
The frontend will be available at <http://localhost:4200>
The frontend will be available at http://localhost:4200
### Local Stripe integration
1. Install the Stripe CLI
<https://stripe.com/docs/stripe-cli>
https://stripe.com/docs/stripe-cli
2. Login to the casino stripe account
```
stripe login --api-key <casino-stripe-secret-key>
```
3. Start webhook forwarding
```
stripe listen --forward-to localhost:8080/webhook
```
@ -131,7 +114,6 @@ stripe listen --forward-to localhost:8080/webhook
### Postgres Management
#### Database cleanup (if needed)
```bash
cd docker
docker-compose down
@ -140,7 +122,6 @@ docker-compose up -d
```
#### Setting up IntelliJ Database View
1. Run the Docker container with PostgreSQL database
2. Open `application.properties` in the resources folder and copy the database URL
3. Open the Database tab in IntelliJ
@ -167,7 +148,6 @@ We follow semantic commit messages to maintain clear project history.
Format: `<type>(<scope>): <subject>`
Where `<type>` is one of:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
@ -177,7 +157,6 @@ Where `<type>` is one of:
- `chore`: Updating build tasks, etc; no production code change
Examples:
```
feat: add user balance display
fix(auth): resolve token expiration issue
@ -185,7 +164,6 @@ docs: update API documentation
```
References:
- [Conventional Commits](https://www.conventionalcommits.org/)
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)

View file

@ -1,137 +1,59 @@
# Casino Gaming Platform - Backend API
# Starter für das LF08 Projekt
A Spring Boot backend application providing REST APIs for a casino gaming platform with multiple games, user management, authentication, and payment processing.
## Requirements
* Docker https://docs.docker.com/get-docker/
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
## Features
### Games
- **Blackjack** - Classic card game with deck management
- **Coinflip** - Simple heads/tails betting game
- **Dice** - Dice rolling game
- **Slots** - Slot machine with symbols and payouts
- **Lootboxes** - Reward system with configurable prizes
### User Management
- User registration and authentication
- OAuth2 integration (Google, GitHub)
- Email verification and password reset
- Balance management and transaction history
### Payment System
- Deposit functionality with webhook support
- Transaction tracking and status management
- Balance updates and fund validation
## Tech Stack
- **Java 17** with Spring Boot
- **Spring Security** with JWT authentication
- **Spring Data JPA** with PostgreSQL
- **OAuth2** for social login
- **Email service** for notifications
- **OpenAPI/Swagger** for API documentation
## Build & Run
### Prerequisites
- Java 17+
- Gradle
- Docker & Docker Compose (for PostgreSQL)
### Build Commands
```bash
# Build the application
./gradlew build
# Clean build
./gradlew clean build
# Run the application
./gradlew bootRun
# Generate JAR file
./gradlew bootJar
## Endpunkt
```
http://localhost:8080
```
## Swagger
```
http://localhost:8080/swagger
```
### Testing
# Postgres
### Terminal öffnen
für alles gilt, im Terminal im Ordner docker/local sein
```bash
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests "FullyQualifiedClassName"
# Run checkstyle
./gradlew checkstyleMain checkstyleTest
```
## API Endpoints
The application runs on `http://localhost:8080`
### API Documentation
- **Swagger UI**: `http://localhost:8080/swagger-ui.html`
- **OpenAPI Spec**: `http://localhost:8080/v3/api-docs`
### Main Endpoints
- `/api/auth/**` - Authentication and user management
- `/api/games/blackjack/**` - Blackjack game operations
- `/api/games/coinflip/**` - Coinflip game operations
- `/api/games/dice/**` - Dice game operations
- `/api/games/slots/**` - Slot machine operations
- `/api/lootboxes/**` - Lootbox management
- `/api/deposits/**` - Payment and deposit handling
- `/api/users/**` - User profile management
- `/api/health` - Health check endpoint
## Database Setup
### PostgreSQL with Docker
```bash
# Start PostgreSQL container
cd docker/local
```
### Postgres starten
```bash
docker compose up
```
Achtung: Der Docker-Container läuft dauerhaft! Wenn er nicht mehr benötigt wird, sollten Sie ihn stoppen.
# Stop PostgreSQL container
### Postgres stoppen
```bash
docker compose down
```
# Reset database (if needed)
### Postgres Datenbank wipen, z.B. bei Problemen
```bash
docker compose down
docker volume rm local_lf8_starter_postgres_data
docker compose up
```
### Database Configuration
Database connection settings are configured in `src/main/resources/application.properties`
### Intellij-Ansicht für Postgres Datenbank einrichten
```bash
1. Lasse den Docker-Container mit der PostgreSQL-Datenbank laufen
2. im Ordner resources die Datei application.properties öffnen und die URL der Datenbank kopieren
3. rechts im Fenster den Reiter Database öffnen
4. In der Database-Symbolleiste auf das Datenbanksymbol mit dem Schlüssel klicken
5. auf das Pluszeichen klicken
6. Datasource from URL auswählen
7. URL der DB einfügen und PostgreSQL-Treiber auswählen, mit OK bestätigen
8. Username lf8_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen
9. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf8_starter_db und public Häkchen setzen
10. mit Apply und ok bestätigen
```
# Keycloak
### IntelliJ Database Setup
1. Start the PostgreSQL Docker container
2. Open `application.properties` and copy the database URL
3. In IntelliJ, open the Database tab (right panel)
4. Click the database icon with key in the toolbar
5. Click the plus (+) icon
6. Select "Datasource from URL"
7. Paste the database URL and select PostgreSQL driver
8. Enter credentials (username: `lf8_starter`, password: `secret`)
9. In Schemas tab, uncheck all except `lf8_starter_db` and `public`
10. Apply and confirm
## Authentication
The application supports multiple authentication methods:
- JWT-based authentication
- OAuth2 (Google, GitHub)
- Email/password with verification
### Getting Bearer Token
For API testing, use the provided HTTP client file:
1. Open `GetBearerToken.http` at project root
2. Execute the request
3. Copy the `access_token` from the response
4. Use in Authorization header: `Bearer <token>`
## Configuration
Key configuration files:
- `application.properties` - Main application configuration
- `SecurityConfig.java` - Security and CORS settings
- `OpenAPIConfiguration.java` - API documentation setup
### Keycloak Token
1. Auf der Projektebene [GetBearerToken.http](../GetBearerToken.http) öffnen.
2. Neben der Request auf den grünen Pfeil drücken
3. Aus dem Reponse das access_token kopieren

View file

@ -1,6 +1,6 @@
plugins {
java
id("org.springframework.boot") version "3.5.0"
id("org.springframework.boot") version "3.4.5"
id("io.spring.dependency-management") version "1.1.7"
id("checkstyle")
}
@ -39,7 +39,7 @@ repositories {
}
dependencies {
implementation("com.stripe:stripe-java:29.2.0")
implementation("com.stripe:stripe-java:29.1.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
@ -47,15 +47,14 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.4.5")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.5")
runtimeOnly("org.postgresql:postgresql")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8")
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("org.springframework.boot:spring-boot-starter-mail")
runtimeOnly("com.h2database:h2")
}
tasks.withType<Test> {

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View file

@ -74,7 +74,8 @@ public class CasinoApplication {
rewardRepository.saveAll(Arrays.asList(
commonReward, rareReward, epicReward,
premiumCommon, premiumRare, legendaryReward));
premiumCommon, premiumRare, legendaryReward
));
basicLootBox.getRewards().add(commonReward);
basicLootBox.getRewards().add(premiumRare);

View file

@ -1,7 +1,9 @@
package de.szut.casino.blackjack;
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.shared.dto.BetDto;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
@ -10,59 +12,121 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
import java.util.Optional;
@Slf4j
@RestController
public class BlackJackGameController {
private final BalanceService balanceService;
private final UserService userService;
private final BlackJackService blackJackService;
public BlackJackGameController(UserService userService, BlackJackService blackJackService) {
public BlackJackGameController(BalanceService balanceService, UserService userService, BlackJackService blackJackService) {
this.balanceService = balanceService;
this.blackJackService = blackJackService;
this.userService = userService;
}
@GetMapping("/blackjack/{id}")
public ResponseEntity<Object> getGame(@PathVariable Long id) {
BlackJackGameEntity game = getBlackJackGame(id);
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(game);
}
@PostMapping("/blackjack/{id}/hit")
public ResponseEntity<Object> hit(@PathVariable Long id) {
BlackJackGameEntity game = getBlackJackGame(id);
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(blackJackService.hit(game));
}
@PostMapping("/blackjack/{id}/stand")
public ResponseEntity<Object> stand(@PathVariable Long id) {
BlackJackGameEntity game = getBlackJackGame(id);
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(blackJackService.stand(game));
}
@PostMapping("/blackjack/{id}/doubleDown")
public ResponseEntity<Object> doubleDown(@PathVariable Long id) {
BlackJackGameEntity game = getBlackJackGame(id);
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(blackJackService.doubleDown(game));
}
@PostMapping("/blackjack/start")
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto) {
return ResponseEntity.ok(blackJackService.createBlackJackGame(betDto));
}
@PostMapping("/blackjack/{id}/split")
public ResponseEntity<Object> split(@PathVariable Long id) {
Optional<UserEntity> optionalUser = userService.getCurrentUser();
private BlackJackGameEntity getBlackJackGame(Long gameId) {
UserEntity user = userService.getCurrentUser();
BlackJackGameEntity game = blackJackService.getBlackJackGame(gameId);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
throw new UserBlackJackGameMismatchException(gameId);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
return game;
UserEntity user = optionalUser.get();
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(blackJackService.split(game));
}
@PostMapping("/blackjack/start")
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto) {
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, betDto)) {
throw new InsufficientFundsException();
}
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betDto.getBetAmount()));
}
}

View file

@ -51,4 +51,15 @@ public class BlackJackGameEntity {
@JsonManagedReference
@SQLRestriction("card_type = 'DEALER'")
private List<CardEntity> dealerCards = new ArrayList<>();
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
@SQLRestriction("card_type = 'PLAYER_SPLIT'")
private List<CardEntity> playerSplitCards = new ArrayList<>();
@Column(name = "split_bet")
private BigDecimal splitBet;
@Column(name = "is_split")
private boolean isSplit;
}

View file

@ -1,37 +1,23 @@
package de.szut.casino.blackjack;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.shared.dto.BetDto;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.UserService;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;
@Service
public class BlackJackService {
private final BlackJackGameRepository blackJackGameRepository;
private final UserRepository userRepository;
private final BalanceService balanceService;
private final UserService userService;
private final DeckService deckService;
private final Random random = new Random();
public BlackJackService(
BlackJackGameRepository blackJackGameRepository,
UserRepository userRepository,
BalanceService balanceService,
UserService userService,
DeckService deckService
) {
public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) {
this.blackJackGameRepository = blackJackGameRepository;
this.userRepository = userRepository;
this.balanceService = balanceService;
this.userService = userService;
this.deckService = deckService;
}
public BlackJackGameEntity getBlackJackGame(Long id) {
@ -39,23 +25,16 @@ public class BlackJackService {
}
@Transactional
public BlackJackGameEntity createBlackJackGame(BetDto betDto) {
UserEntity user = userService.getCurrentUser();
if (!this.balanceService.hasFunds(user, betDto)) {
throw new InsufficientFundsException();
}
this.balanceService.subtractFunds(user, betDto.getBetAmount());
public BlackJackGameEntity createBlackJackGame(UserEntity user, BigDecimal betAmount) {
BlackJackGameEntity game = new BlackJackGameEntity();
game.setUser(user);
game.setBet(betDto.getBetAmount());
game.setBet(betAmount);
this.deckService.initializeDeck(game);
this.deckService.dealInitialCards(game);
initializeDeck(game);
dealInitialCards(game);
game.setState(getState(game));
deductBetFromBalance(user, betAmount);
return processGameBasedOnState(game);
}
@ -66,7 +45,7 @@ public class BlackJackService {
return game;
}
this.deckService.dealCardToPlayer(game);
dealCardToPlayer(game);
updateGameStateAndBalance(game);
return processGameBasedOnState(game);
@ -90,14 +69,13 @@ public class BlackJackService {
return game;
}
UserEntity user = game.getUser();
UserEntity user = getUserWithFreshData(game.getUser());
BigDecimal additionalBet = game.getBet();
this.balanceService.subtractFunds(user, additionalBet);
deductBetFromBalance(user, additionalBet);
game.setBet(game.getBet().add(additionalBet));
this.deckService.dealCardToPlayer(game);
dealCardToPlayer(game);
updateGameStateAndBalance(game);
if (game.getState() == BlackJackState.IN_PROGRESS) {
@ -107,6 +85,36 @@ public class BlackJackService {
return game;
}
@Transactional
public BlackJackGameEntity split(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS ||
game.getPlayerCards().size() != 2 ||
game.isSplit() ||
!game.getPlayerCards().get(0).getRank().equals(game.getPlayerCards().get(1).getRank())) {
return game;
}
UserEntity user = getUserWithFreshData(game.getUser());
BigDecimal splitBet = game.getBet();
if (user.getBalance().compareTo(splitBet) < 0) {
return game;
}
deductBetFromBalance(user, splitBet);
game.setSplitBet(splitBet);
game.setSplit(true);
CardEntity card = game.getPlayerCards().remove(1);
card.setCardType(CardType.PLAYER_SPLIT);
game.getPlayerSplitCards().add(card);
dealCardToPlayer(game);
dealCardToSplitHand(game);
return processGameBasedOnState(game);
}
private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS) {
this.blackJackGameRepository.delete(game);
@ -116,13 +124,67 @@ public class BlackJackService {
return blackJackGameRepository.save(game);
}
private void updateGameStateAndBalance(BlackJackGameEntity game) {
game.setState(getState(game));
private BlackJackGameEntity refreshGameState(BlackJackGameEntity game) {
return blackJackGameRepository.findById(game.getId()).orElse(game);
}
if (game.getState() == BlackJackState.PLAYER_WON) {
updateUserBalance(game, true);
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
updateUserBalance(game, false);
private UserEntity getUserWithFreshData(UserEntity user) {
return userRepository.findById(user.getId()).orElse(user);
}
private void dealInitialCards(BlackJackGameEntity game) {
for (int i = 0; i < 2; i++) {
dealCardToPlayer(game);
}
dealCardToDealer(game);
}
private void dealCardToPlayer(BlackJackGameEntity game) {
CardEntity card = drawCardFromDeck(game);
card.setCardType(CardType.PLAYER);
game.getPlayerCards().add(card);
}
private void dealCardToDealer(BlackJackGameEntity game) {
CardEntity card = drawCardFromDeck(game);
card.setCardType(CardType.DEALER);
game.getDealerCards().add(card);
}
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
while (calculateHandValue(game.getDealerCards()) < 17) {
dealCardToDealer(game);
}
}
private void dealCardToSplitHand(BlackJackGameEntity game) {
CardEntity card = drawCardFromDeck(game);
card.setCardType(CardType.PLAYER_SPLIT);
game.getPlayerSplitCards().add(card);
}
private void updateGameStateAndBalance(BlackJackGameEntity game) {
if (game.isSplit()) {
int mainHandValue = calculateHandValue(game.getPlayerCards());
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
if (mainHandValue > 21 && splitHandValue > 21) {
game.setState(BlackJackState.PLAYER_LOST);
updateUserBalance(game, false);
} else if (mainHandValue <= 21 && splitHandValue <= 21) {
game.setState(BlackJackState.IN_PROGRESS);
} else {
game.setState(BlackJackState.IN_PROGRESS);
}
} else {
game.setState(getState(game));
if (game.getState() == BlackJackState.PLAYER_WON) {
updateUserBalance(game, true);
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
updateUserBalance(game, false);
}
}
}
@ -142,26 +204,79 @@ public class BlackJackService {
}
}
protected void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
UserEntity user = game.getUser();
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
user.setBalance(user.getBalance().subtract(betAmount));
userRepository.save(user);
}
@Transactional
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
UserEntity user = getUserWithFreshData(game.getUser());
BigDecimal totalBet = game.getBet();
BigDecimal balance = user.getBalance();
if (isWin) {
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
} else if (game.getState() == BlackJackState.DRAW) {
balance = balance.add(totalBet);
if (game.isSplit()) {
totalBet = totalBet.add(game.getSplitBet());
if (isWin) {
int mainHandValue = calculateHandValue(game.getPlayerCards());
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
int dealerValue = calculateHandValue(game.getDealerCards());
if (mainHandValue <= 21 && (dealerValue > 21 || mainHandValue > dealerValue)) {
balance = balance.add(game.getBet().multiply(BigDecimal.valueOf(2)));
} else if (mainHandValue == dealerValue) {
balance = balance.add(game.getBet());
}
if (splitHandValue <= 21 && (dealerValue > 21 || splitHandValue > dealerValue)) {
balance = balance.add(game.getSplitBet().multiply(BigDecimal.valueOf(2)));
} else if (splitHandValue == dealerValue) {
balance = balance.add(game.getSplitBet());
}
} else if (game.getState() == BlackJackState.DRAW) {
balance = balance.add(totalBet);
}
} else {
if (isWin) {
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
} else if (game.getState() == BlackJackState.DRAW) {
balance = balance.add(totalBet);
}
}
user.setBalance(balance);
userRepository.save(user);
}
private void initializeDeck(BlackJackGameEntity game) {
for (Suit suit : Suit.values()) {
for (Rank rank : Rank.values()) {
CardEntity card = new CardEntity();
card.setGame(game);
card.setSuit(suit);
card.setRank(rank);
card.setCardType(CardType.DECK);
game.getDeck().add(card);
}
}
java.util.Collections.shuffle(game.getDeck(), random);
}
private CardEntity drawCardFromDeck(BlackJackGameEntity game) {
if (game.getDeck().isEmpty()) {
throw new IllegalStateException("Deck is empty");
}
return game.getDeck().removeFirst();
}
private BlackJackState getState(BlackJackGameEntity game) {
int playerHandValue = calculateHandValue(game.getPlayerCards());
if (playerHandValue == 21) {
CardEntity hole = this.deckService.drawCardFromDeck(game);
CardEntity hole = drawCardFromDeck(game);
hole.setCardType(CardType.DEALER);
game.getDealerCards().add(hole);
@ -171,7 +286,7 @@ public class BlackJackService {
return BlackJackState.DRAW;
} else {
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
UserEntity user = game.getUser();
UserEntity user = getUserWithFreshData(game.getUser());
user.setBalance(user.getBalance().add(blackjackWinnings));
return BlackJackState.PLAYER_BLACKJACK;
}
@ -199,12 +314,4 @@ public class BlackJackService {
return sum;
}
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
while (calculateHandValue(game.getDealerCards()) < 17) {
this.deckService.dealCardToDealer(game);
}
}
}

View file

@ -36,5 +36,5 @@ public class CardEntity {
}
enum CardType {
DECK, PLAYER, DEALER
DECK, PLAYER, DEALER, PLAYER_SPLIT
}

View file

@ -1,57 +0,0 @@
package de.szut.casino.blackjack;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class DeckService {
private final Random random;
public DeckService(Random random) {
this.random = random;
}
public void initializeDeck(BlackJackGameEntity game) {
for (Suit suit : Suit.values()) {
for (Rank rank : Rank.values()) {
CardEntity card = new CardEntity();
card.setGame(game);
card.setSuit(suit);
card.setRank(rank);
card.setCardType(CardType.DECK);
game.getDeck().add(card);
}
}
java.util.Collections.shuffle(game.getDeck(), random);
}
public CardEntity drawCardFromDeck(BlackJackGameEntity game) {
if (game.getDeck().isEmpty()) {
throw new IllegalStateException("Deck is empty");
}
return game.getDeck().removeFirst();
}
public void dealInitialCards(BlackJackGameEntity game) {
for (int i = 0; i < 2; i++) {
dealCardToPlayer(game);
}
dealCardToDealer(game);
}
public void dealCardToPlayer(BlackJackGameEntity game) {
CardEntity card = drawCardFromDeck(game);
card.setCardType(CardType.PLAYER);
game.getPlayerCards().add(card);
}
public void dealCardToDealer(BlackJackGameEntity game) {
CardEntity card = drawCardFromDeck(game);
card.setCardType(CardType.DEALER);
game.getDealerCards().add(card);
}
}

View file

@ -28,7 +28,13 @@ public class CoinflipController {
@PostMapping("/coinflip")
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
UserEntity user = userService.getCurrentUser();
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, coinflipDto)) {
throw new InsufficientFundsException();

View file

@ -2,16 +2,13 @@ package de.szut.casino.coinflip;
import de.szut.casino.shared.dto.BetDto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
public class CoinflipDto extends BetDto {
@NotNull(message = "chosen side cannot be null")
private CoinSide coinSide;

View file

@ -9,12 +9,11 @@ import java.util.Random;
@Service
public class CoinflipService {
private final Random random;
private final Random random = new Random();
private final BalanceService balanceService;
public CoinflipService(BalanceService balanceService, Random random) {
public CoinflipService(BalanceService balanceService) {
this.balanceService = balanceService;
this.random = random;
}
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {

View file

@ -1,15 +0,0 @@
package de.szut.casino.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Random;
@Configuration
public class AppConfig {
@Bean
public Random random() {
return new Random();
}
}

View file

@ -16,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class DepositController {
@ -27,7 +29,7 @@ public class DepositController {
private final TransactionService transactionService;
private final UserService userService;
private UserService userService;
public DepositController(TransactionService transactionService, UserService userService) {
this.transactionService = transactionService;
@ -38,7 +40,7 @@ public class DepositController {
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
Stripe.apiKey = stripeKey;
UserEntity user = userService.getCurrentUser();
Optional<UserEntity> optionalUserEntity = this.userService.getCurrentUser();
SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder()
@ -58,7 +60,11 @@ public class DepositController {
Session session = Session.create(params);
transactionService.createTransaction(user, session.getId(), amountDto.getAmount());
if (optionalUserEntity.isEmpty()) {
throw new RuntimeException("User doesnt exist");
}
transactionService.createTransaction(optionalUserEntity.get(), session.getId(), amountDto.getAmount());
return ResponseEntity.ok(new SessionIdDto(session.getId()));
}

View file

@ -27,7 +27,13 @@ public class DiceController {
@PostMapping("/dice")
public ResponseEntity<Object> rollDice(@RequestBody @Valid DiceDto diceDto) {
UserEntity user = userService.getCurrentUser();
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, diceDto)) {
throw new InsufficientFundsException();

View file

@ -5,14 +5,12 @@ import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
public class DiceDto extends BetDto {
private boolean rollOver;

View file

@ -11,11 +11,10 @@ import java.util.Random;
@Service
public class DiceService {
private static final int MAX_DICE_VALUE = 100;
private final Random random;
private final Random random = new Random();
private final BalanceService balanceService;
public DiceService(Random random, BalanceService balanceService) {
this.random = random;
public DiceService(BalanceService balanceService) {
this.balanceService = balanceService;
}

View file

@ -2,7 +2,6 @@ package de.szut.casino.exceptionHandling;
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import jakarta.persistence.EntityExistsException;
import org.springframework.http.HttpStatus;
@ -39,10 +38,4 @@ public class GlobalExceptionHandler {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(UserBlackJackGameMismatchException.class)
public ResponseEntity<?> handleUserBlackJackGameMismatchException(UserBlackJackGameMismatchException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
}

View file

@ -1,7 +0,0 @@
package de.szut.casino.exceptionHandling.exceptions;
public class UserBlackJackGameMismatchException extends RuntimeException {
public UserBlackJackGameMismatchException(Long gameId) {
super(String.format("Blackjack game with ID %d not found or does not belong to the current user.", gameId));
}
}

View file

@ -38,7 +38,13 @@ public class LootBoxController {
}
LootBoxEntity lootBox = optionalLootBox.get();
UserEntity user = userService.getCurrentUser();
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
throw new InsufficientFundsException();

View file

@ -25,7 +25,7 @@ public class RewardEntity {
@GeneratedValue
private Long id;
@Column(precision = 19, scale = 2, name = "rewardValue")
@Column(precision = 19, scale = 2)
private BigDecimal value;
@Column(precision = 5, scale = 2)

View file

@ -9,6 +9,7 @@ import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import jakarta.mail.MessagingException;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -19,11 +20,8 @@ import java.io.IOException;
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException {

View file

@ -1,8 +1,10 @@
package de.szut.casino.security.oauth2.github;
package de.szut.casino.security;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.dto.GithubCallbackDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -22,11 +24,8 @@ public class GitHubController {
@Value("${spring.security.oauth2.client.registration.github.redirect-uri}")
private String redirectUri;
private final GitHubService githubService;
public GitHubController(GitHubService githubService) {
this.githubService = githubService;
}
@Autowired
private GitHubService githubService;
@GetMapping("/authorize")
public RedirectView authorizeGithub() {

View file

@ -1,13 +1,13 @@
package de.szut.casino.security.oauth2.github;
package de.szut.casino.security;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionRepository;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.jwt.JwtUtils;
import de.szut.casino.user.AuthProvider;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@ -31,19 +31,17 @@ public class GitHubService {
@Value("${spring.security.oauth2.client.registration.github.client-secret}")
private String clientSecret;
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final TransactionRepository transactionRepository;
private final JwtUtils jwtUtils;
private final PasswordEncoder oauth2PasswordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.transactionRepository = transactionRepository;
this.jwtUtils = jwtUtils;
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
}
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private PasswordEncoder oauth2PasswordEncoder;
public AuthResponseDto processGithubCode(String code) {
try {
@ -144,20 +142,15 @@ public class GitHubService {
user.setProvider(AuthProvider.GITHUB);
user.setProviderId(githubId);
user.setEmailVerified(true);
user.setBalance(new BigDecimal("100.00"));
user.setBalance(new BigDecimal("1000.00"));
}
}
String randomPassword = UUID.randomUUID().toString();
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
userRepository.save(user);
transactionRepository.save(transaction);
Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword));

View file

@ -1,9 +1,10 @@
package de.szut.casino.security.oauth2.google;
package de.szut.casino.security;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.oauth2.github.GithubCallbackDto;
import de.szut.casino.security.dto.GithubCallbackDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -23,11 +24,8 @@ public class GoogleController {
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
private String redirectUri;
private final GoogleService googleService;
public GoogleController(GoogleService googleService) {
this.googleService = googleService;
}
@Autowired
private GoogleService googleService;
@GetMapping("/authorize")
public RedirectView authorizeGoogle() {

View file

@ -1,8 +1,5 @@
package de.szut.casino.security.oauth2.google;
package de.szut.casino.security;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionRepository;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.jwt.JwtUtils;
import de.szut.casino.user.AuthProvider;
@ -10,6 +7,7 @@ import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@ -25,9 +23,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
@Service
public class GoogleService {
@ -48,19 +44,17 @@ public class GoogleService {
@Value("${spring.security.oauth2.client.provider.google.user-info-uri}")
private String userInfoUri;
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final TransactionRepository transactionRepository;
private final JwtUtils jwtUtils;
private final PasswordEncoder oauth2PasswordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.transactionRepository = transactionRepository;
this.jwtUtils = jwtUtils;
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
}
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private PasswordEncoder oauth2PasswordEncoder;
public AuthResponseDto processGoogleCode(String code) {
try {
@ -151,14 +145,8 @@ public class GoogleService {
String randomPassword = UUID.randomUUID().toString();
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
userRepository.save(user);
transactionRepository.save(transaction);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)

View file

@ -1,6 +1,7 @@
package de.szut.casino.security;
import de.szut.casino.security.jwt.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -31,13 +32,11 @@ public class SecurityConfig {
@Value("${app.frontend-host}")
private String frontendHost;
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean

View file

@ -1,4 +1,4 @@
package de.szut.casino.security.oauth2.github;
package de.szut.casino.security.dto;
import lombok.Data;

View file

@ -4,6 +4,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
@ -18,13 +19,11 @@ import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
@Autowired
private JwtUtils jwtUtils;
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsService userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

View file

@ -91,7 +91,7 @@ public class JwtUtils {
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)

View file

@ -4,6 +4,7 @@ import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessin
import de.szut.casino.user.AuthProvider;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -21,13 +22,11 @@ import java.util.UUID;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final PasswordEncoder oauth2PasswordEncoder;
@Autowired
private UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository, PasswordEncoder oauth2PasswordEncoder) {
this.userRepository = userRepository;
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
}
@Autowired
private PasswordEncoder oauth2PasswordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {

View file

@ -1,6 +1,4 @@
package de.szut.casino.security.oauth2.github;
import de.szut.casino.security.oauth2.OAuth2UserInfo;
package de.szut.casino.security.oauth2;
import java.util.Map;

View file

@ -1,6 +1,4 @@
package de.szut.casino.security.oauth2.google;
import de.szut.casino.security.oauth2.OAuth2UserInfo;
package de.szut.casino.security.oauth2;
import java.util.Map;

View file

@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
@ -20,11 +21,8 @@ public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
@Value("${app.oauth2.authorizedRedirectUris}")
private String redirectUri;
private final JwtUtils jwtUtils;
public OAuth2AuthenticationSuccessHandler(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Autowired
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)

View file

@ -1,8 +1,6 @@
package de.szut.casino.security.oauth2;
import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException;
import de.szut.casino.security.oauth2.github.GitHubOAuth2UserInfo;
import de.szut.casino.security.oauth2.google.GoogleOAuth2UserInfo;
import de.szut.casino.user.AuthProvider;
import java.util.Map;

View file

@ -11,6 +11,7 @@ import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import jakarta.mail.MessagingException;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@ -24,19 +25,20 @@ import java.util.Optional;
@Service
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
private final UserService userService;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public AuthService(AuthenticationManager authenticationManager, JwtUtils jwtUtils, UserService userService, EmailService emailService, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.jwtUtils = jwtUtils;
this.userService = userService;
this.emailService = emailService;
this.passwordEncoder = passwordEncoder;
}
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserService userService;
@Autowired
private EmailService emailService;
@Autowired
private PasswordEncoder passwordEncoder;
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {

View file

@ -28,7 +28,6 @@ public class EmailService {
this.mailConfig = mailConfig;
this.mailSender.setHost(mailConfig.host);
this.mailSender.setPort(mailConfig.port);
this.mailSender.setProtocol(mailConfig.protocol);
if (mailConfig.authenticationEnabled) {
this.mailSender.setUsername(mailConfig.username);
this.mailSender.setPassword(mailConfig.password);

View file

@ -22,7 +22,4 @@ public class MailConfig {
@Value("${app.mail.from-address}")
public String fromAddress;
@Value("${app.mail.protocol}")
public String protocol;
}

View file

@ -2,6 +2,7 @@ package de.szut.casino.security.service;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@ -13,11 +14,8 @@ import java.util.Optional;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {

View file

@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@ -12,7 +11,6 @@ import java.math.BigDecimal;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BetDto {
@NotNull(message = "Bet amount cannot be null")
@Positive(message = "Bet amount must be positive")

View file

@ -32,7 +32,13 @@ public class SlotController {
@PostMapping("/slots/spin")
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto) {
UserEntity user = userService.getCurrentUser();
Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, betDto)) {
throw new InsufficientFundsException();

View file

@ -2,6 +2,7 @@ package de.szut.casino.user;
import de.szut.casino.user.dto.GetUserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
@ -14,17 +15,14 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
private UserService userService;
private final UserMappingService userMappingService;
public UserController(UserService userService, UserMappingService userMappingService) {
this.userService = userService;
this.userMappingService = userMappingService;
}
@Autowired
private UserMappingService userMappingService;
@GetMapping("/me")
public ResponseEntity<GetUserDto> getCurrentUser() {
return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser()));
return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser().orElseThrow()));
}
}

View file

@ -16,9 +16,6 @@ public class UserEntity {
@GeneratedValue
private Long id;
@Version
private Long version;
@Column(unique = true)
private String email;
@ -47,6 +44,7 @@ public class UserEntity {
this.password = password;
this.balance = balance;
this.verificationToken = verificationToken;
this.provider = AuthProvider.LOCAL;
}
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {

View file

@ -1,11 +1,9 @@
package de.szut.casino.user;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.dto.CreateUserDto;
import jakarta.persistence.EntityExistsException;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@ -15,13 +13,11 @@ import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Autowired
private PasswordEncoder passwordEncoder;
public UserEntity createUser(CreateUserDto createUserDto) {
if (userRepository.existsByUsername(createUserDto.getUsername())) {
@ -40,24 +36,13 @@ public class UserService {
RandomStringUtils.randomAlphanumeric(64)
);
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
return userRepository.save(user);
}
public UserEntity getCurrentUser() {
public Optional<UserEntity> getCurrentUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
return optionalUser.get();
return userRepository.findByUsername(username);
}
public Optional<UserEntity> getUserByVerificationToken(String token) {

View file

@ -6,29 +6,31 @@ import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import de.szut.casino.user.transaction.dto.GetTransactionDto;
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class GetTransactionService {
private final UserService userService;
@Autowired
private UserService userService;
private final TransactionRepository transactionRepository;
@Autowired
private TransactionRepository transactionRepository;
public GetTransactionService(UserService userService, TransactionRepository transactionRepository) {
this.userService = userService;
this.transactionRepository = transactionRepository;
}
public UserTransactionsDto getUserTransactionsDto(String authToken, Integer limit, Integer offset) {
Optional<UserEntity> user = this.userService.getCurrentUser();
if (user.isPresent()) {
List<TransactionEntity> transactionEntities = this.transactionRepository.findByUserIdWithLimit(user.get(), limit, offset);
Boolean hasMore = this.transactionRepository.hasMore(user.get(), limit, offset);
public UserTransactionsDto getUserTransactionsDto(Integer limit, Integer offset) {
UserEntity user = userService.getCurrentUser();
return new UserTransactionsDto(mapTransactionsToDtos(transactionEntities), hasMore);
}
List<TransactionEntity> transactionEntities = this.transactionRepository.findByUserIdWithLimit(user, limit, offset);
Boolean hasMore = this.transactionRepository.hasMore(user, limit, offset);
return new UserTransactionsDto(mapTransactionsToDtos(transactionEntities), hasMore);
return new UserTransactionsDto(List.of(), false);
}
public List<GetTransactionDto> mapTransactionsToDtos(List<TransactionEntity> transactions) {

View file

@ -1,26 +1,26 @@
package de.szut.casino.user.transaction;
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
private final GetTransactionService transactionService;
public TransactionController(GetTransactionService transactionService) {
this.transactionService = transactionService;
}
@Autowired
private GetTransactionService transactionService;
@GetMapping("/user/transactions")
public ResponseEntity<UserTransactionsDto> getUserTransactions(
@RequestHeader("Authorization") String authToken,
@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "offset", required = false) Integer offset
) {
UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(limit, offset);
UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset);
return ResponseEntity.ok(transactionEntities);
}

View file

@ -1,58 +0,0 @@
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
server.port=${HTTP_PORT:8080}
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

View file

@ -14,7 +14,6 @@ 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

View file

@ -0,0 +1,13 @@
package de.szut.casino;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Lf8StarterApplicationTests {
@Test
void contextLoads() {
}
}

View file

@ -1,64 +0,0 @@
package de.szut.casino.coinflip;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.math.BigDecimal;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class CoinflipServiceTest {
@Mock
private BalanceService balanceService;
@Mock
private Random random;
@InjectMocks
private CoinflipService coinflipService;
private UserEntity user;
private CoinflipDto coinflipDto;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
user = new UserEntity();
user.setBalance(BigDecimal.valueOf(100));
coinflipDto = new CoinflipDto(BigDecimal.valueOf(10), CoinSide.HEAD);
}
@Test
void testPlay_userWins() {
when(random.nextBoolean()).thenReturn(true);
CoinflipResult result = coinflipService.play(user, coinflipDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(20), result.getPayout());
assertEquals(CoinSide.HEAD, result.getCoinSide());
verify(balanceService, times(1)).subtractFunds(user, BigDecimal.valueOf(10));
verify(balanceService, times(1)).addFunds(user, BigDecimal.valueOf(20));
}
@Test
void testPlay_userLoses() {
when(random.nextBoolean()).thenReturn(false);
CoinflipResult result = coinflipService.play(user, coinflipDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.ZERO, result.getPayout());
assertEquals(CoinSide.TAILS, result.getCoinSide());
verify(balanceService, times(1)).subtractFunds(user, BigDecimal.valueOf(10));
verify(balanceService, never()).addFunds(any(), any());
}
}

View file

@ -1,251 +0,0 @@
package de.szut.casino.dice;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DiceServiceTest {
@Mock
private BalanceService balanceService;
@Mock
private Random random;
@InjectMocks
private DiceService diceService;
private UserEntity user;
private DiceDto diceDto;
@BeforeEach
void setUp() {
user = new UserEntity();
user.setId(1L);
user.setBalance(BigDecimal.valueOf(1000));
diceDto = new DiceDto();
diceDto.setBetAmount(BigDecimal.valueOf(10));
diceDto.setTargetValue(BigDecimal.valueOf(50));
diceDto.setRollOver(true);
}
@Test
void play_rollOver_win() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(50));
when(random.nextInt(anyInt())).thenReturn(55);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(56), result.getRolledValue());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollOver_lose() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(50));
when(random.nextInt(anyInt())).thenReturn(49);
DiceResult result = diceService.play(user, diceDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.valueOf(50), result.getRolledValue());
assertEquals(BigDecimal.ZERO, result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollUnder_win() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(50));
when(random.nextInt(anyInt())).thenReturn(48);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(49), result.getRolledValue());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollUnder_lose() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(50));
when(random.nextInt(anyInt())).thenReturn(50);
DiceResult result = diceService.play(user, diceDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.valueOf(51), result.getRolledValue());
assertEquals(BigDecimal.ZERO, result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollOver_targetValueOne_rolledOne_lose() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(1));
when(random.nextInt(anyInt())).thenReturn(0);
DiceResult result = diceService.play(user, diceDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
assertEquals(BigDecimal.ZERO, result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollOver_targetValueOne_rolledTwo_win() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(1));
when(random.nextInt(anyInt())).thenReturn(1);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(2), result.getRolledValue());
// Win chance for target 1 (roll over) is 99. Multiplier = (100-1)/99 = 1
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollUnder_targetValueOne_alwaysLose_winChanceZero() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(1));
when(random.nextInt(anyInt())).thenReturn(0);
DiceResult result = diceService.play(user, diceDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
assertEquals(BigDecimal.ZERO, result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollOver_targetValueNinetyNine_rolledHundred_win() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(99));
when(random.nextInt(anyInt())).thenReturn(99);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
// Win chance for target 99 (roll over) is 1. Multiplier = (100-1)/1 = 99
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99)).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollUnder_targetValueNinetyNine_rolledNinetyEight_win() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(99));
when(random.nextInt(anyInt())).thenReturn(97);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(98), result.getRolledValue());
// Win chance for target 99 (roll under) is 98. Multiplier = (100-1)/98 = 99/98
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99).divide(BigDecimal.valueOf(98), 4, RoundingMode.HALF_UP)), result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollOver_targetValueOneHundred_alwaysLose_winChanceZero() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(100));
when(random.nextInt(anyInt())).thenReturn(99);
DiceResult result = diceService.play(user, diceDto);
assertFalse(result.isWin());
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
assertEquals(BigDecimal.ZERO, result.getPayout());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_rollUnder_targetValueOneHundred_rolledNinetyNine_win() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(100));
when(random.nextInt(anyInt())).thenReturn(98);
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(99), result.getRolledValue());
// Win chance for target 100 (roll under) is 99. Multiplier = (100-1)/99 = 1
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
}
@Test
void play_payoutCalculationCorrect() {
diceDto.setRollOver(true);
diceDto.setTargetValue(BigDecimal.valueOf(75));
when(random.nextInt(anyInt())).thenReturn(75);
// Multiplier for win chance 25: (100-1)/25 = 99/25 = 3.96
// Payout: 10 * 3.96 = 39.6
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(39.6).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
}
@Test
void play_payoutCalculationCorrect_rollUnder() {
diceDto.setRollOver(false);
diceDto.setTargetValue(BigDecimal.valueOf(25));
when(random.nextInt(anyInt())).thenReturn(0);
// Multiplier for win chance 24: (100-1)/24 = 99/24 = 4.125
// Payout: 10 * 4.125 = 41.25
DiceResult result = diceService.play(user, diceDto);
assertTrue(result.isWin());
assertEquals(BigDecimal.valueOf(41.25).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
}
@Test
void play_betAmountSubtracted() {
when(random.nextInt(anyInt())).thenReturn(50);
diceService.play(user, diceDto);
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
}
}

View file

@ -0,0 +1,26 @@
package de.szut.casino.health;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(HealthController.class)
@AutoConfigureMockMvc(addFilters = false)
public class HealthControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void healthCheckReturnsUpStatus() throws Exception {
mockMvc.perform(get("/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
}

View file

@ -1,76 +0,0 @@
package de.szut.casino.shared.service;
import de.szut.casino.shared.dto.BetDto;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class BalanceServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private BalanceService balanceService;
private UserEntity user;
private BetDto betDto;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
user = new UserEntity();
user.setBalance(BigDecimal.valueOf(100));
betDto = new BetDto();
}
@Test
void testHasFunds_sufficientFunds() {
betDto.setBetAmount(BigDecimal.valueOf(50));
assertTrue(balanceService.hasFunds(user, betDto));
}
@Test
void testHasFunds_insufficientFunds() {
betDto.setBetAmount(BigDecimal.valueOf(150));
assertFalse(balanceService.hasFunds(user, betDto));
}
@Test
void testHasFunds_exactFunds() {
betDto.setBetAmount(BigDecimal.valueOf(100));
assertTrue(balanceService.hasFunds(user, betDto));
}
@Test
void testAddFunds() {
BigDecimal amountToAdd = BigDecimal.valueOf(50);
balanceService.addFunds(user, amountToAdd);
assertEquals(BigDecimal.valueOf(150), user.getBalance());
verify(userRepository, times(1)).save(user);
}
@Test
void testSubtractFunds_sufficientFunds() {
BigDecimal amountToSubtract = BigDecimal.valueOf(50);
balanceService.subtractFunds(user, amountToSubtract);
assertEquals(BigDecimal.valueOf(50), user.getBalance());
verify(userRepository, times(1)).save(user);
}
@Test
void testSubtractFunds_insufficientFunds() {
BigDecimal amountToSubtract = BigDecimal.valueOf(150);
assertThrows(IllegalStateException.class, () -> balanceService.subtractFunds(user, amountToSubtract));
verify(userRepository, never()).save(user);
}
}

View file

@ -0,0 +1,122 @@
package de.szut.casino.user;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
@WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
private GetUserDto getUserDto;
private CreateUserDto createUserDto;
private UserEntity testUser;
private final String TEST_ID = "test-id-123";
private final String AUTH_TOKEN = "Bearer test-token";
@BeforeEach
void setUp() {
getUserDto = new GetUserDto();
getUserDto.setAuthentikId(TEST_ID);
getUserDto.setUsername("testuser");
testUser = new UserEntity();
testUser.setAuthentikId(TEST_ID);
testUser.setUsername("testuser");
createUserDto = new CreateUserDto();
createUserDto.setAuthentikId(TEST_ID);
createUserDto.setUsername("testuser");
}
@Test
void getUserByIdSuccess() throws Exception {
when(userService.exists(TEST_ID)).thenReturn(true);
when(userService.getUser(TEST_ID)).thenReturn(getUserDto);
mockMvc.perform(get("/user/" + TEST_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
.andExpect(jsonPath("$.username").value("testuser"));
}
@Test
void getUserByIdNotFound() throws Exception {
when(userService.exists(TEST_ID)).thenReturn(false);
mockMvc.perform(get("/user/" + TEST_ID))
.andExpect(status().isNotFound());
}
@Test
void createUserSuccess() throws Exception {
when(userService.exists(TEST_ID)).thenReturn(false);
when(userService.createUser(any(CreateUserDto.class))).thenReturn(testUser);
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createUserDto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
.andExpect(jsonPath("$.username").value("testuser"));
}
@Test
void createUserAlreadyExists() throws Exception {
when(userService.exists(TEST_ID)).thenReturn(true);
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createUserDto)))
.andExpect(status().isFound())
.andExpect(header().string("Location", "/user/" + TEST_ID));
}
@Test
void getCurrentUserSuccess() throws Exception {
when(userService.getCurrentUser(AUTH_TOKEN)).thenReturn(getUserDto);
mockMvc.perform(get("/user")
.header("Authorization", AUTH_TOKEN))
.andExpect(status().isOk())
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
.andExpect(jsonPath("$.username").value("testuser"));
}
@Test
void getCurrentUserNotFound() throws Exception {
when(userService.getCurrentUser(anyString())).thenReturn(null);
mockMvc.perform(get("/user")
.header("Authorization", AUTH_TOKEN))
.andExpect(status().isNotFound());
}
}

View file

@ -1,12 +1,7 @@
FROM oven/bun:debian AS build
WORKDIR /app
RUN apt-get update -y && \
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update -y && apt-get install nodejs -y
ENV NODE_ENV=production

3
frontend/.gitignore vendored
View file

@ -29,9 +29,6 @@ yarn-error.log
.history/*
# Miscellaneous
/.claude
/test-results
/playwright-report
/.angular/cache
.sass-cache/
/connect.lock

View file

@ -1,106 +1,18 @@
# Casino Gaming Platform - Frontend
A modern Angular 20 casino gaming platform featuring multiple games including Blackjack, Coinflip, Dice, Slots, and Lootboxes. Built with Angular 20, TailwindCSS 4, and powered by Bun for fast development.
This is the frontend application for the Casino Gaming Platform. It's built with Angular 18 and TailwindCSS, providing a responsive and modern UI for the casino gaming experience.
## 🎮 Features
## Development
- **Multiple Games**: Blackjack, Coinflip, Dice, Slots, Lootboxes
- **User Authentication**: OAuth2, email verification, password recovery
- **Real-time Gaming**: Interactive game mechanics with animations
- **Payment Integration**: Stripe integration for deposits
- **Responsive Design**: Mobile-first design with TailwindCSS
- **Audio Experience**: Game sounds and audio feedback
- **Transaction History**: Complete betting and transaction tracking
### Commands
## 🚀 Getting Started
### Prerequisites
- [Bun](https://bun.sh/) (recommended) or Node.js 18+
- Angular CLI 20+
### Installation
```bash
# Install dependencies
bun install
# Start development server
bun run start
```
The app will be available at `http://localhost:4200`
## 📋 Commands
### Development
- **Start Dev Server**: `bun run start` - Starts dev server with proxy configuration
- **Build**: `bun run build` - Production build
- **Watch Build**: `bun run watch` - Development build with file watching
### Code Quality
- **Format**: `bun run format` - Format code with Prettier
- **Format Check**: `bun run format:check` - Check code formatting
- **Lint**: `bun run lint` - Run ESLint
- **OxLint**: `bun run oxlint` - Run OxLint with strict warnings
### Testing
- **Test All**: `bun run test` - Run all tests with Karma/Jasmine
- **Build**: `bun run build` or `bunx @angular/cli build`
- **Start Dev Server**: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
- **Format Code**: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
- **Lint**: `bun run lint` or `ng lint`
- **Test**: `bun run test` or `bunx @angular/cli test`
- **Test Single File**: `bunx @angular/cli test --include=path/to/test.spec.ts`
## 🛠️ Technology Stack
### Core
- **Angular 20**: Latest Angular framework with standalone components
- **TypeScript 5.8**: Strongly typed JavaScript
- **RxJS 7.8**: Reactive programming for HTTP and state management
### Styling & UI
- **TailwindCSS 4**: Utility-first CSS framework
- **PostCSS**: CSS processing and optimization
- **FontAwesome**: Icon library with Angular integration
### Animation & Interaction
- **GSAP**: High-performance animations
- **CountUp.js**: Number animation effects
- **Custom Audio Service**: Game sound effects and feedback
### Development Tools
- **Bun**: Fast JavaScript runtime and package manager
- **ESLint + Angular ESLint**: Code linting with Angular-specific rules
- **OxLint**: Fast Rust-based linter
- **Prettier**: Code formatting
- **Karma + Jasmine**: Testing framework
### Payment & APIs
- **Stripe**: Payment processing integration
- **Custom HTTP Interceptors**: API communication and error handling
## 🏗️ Architecture
### Project Structure
```
src/
├── app/
│ ├── feature/ # Feature modules
│ │ ├── auth/ # Authentication (login, register, OAuth2)
│ │ ├── game/ # Game modules (blackjack, coinflip, dice, slots)
│ │ ├── lootboxes/ # Lootbox system
│ │ └── deposit/ # Payment and deposits
│ ├── model/ # Data models and interfaces
│ ├── service/ # Core services (auth, user, transaction)
│ └── shared/ # Shared components, directives, services
├── environments/ # Environment configurations
└── public/ # Static assets (images, sounds)
```
### Key Components
- **Game Components**: Modular game implementations with services
- **Shared Components**: Reusable UI components (navbar, footer, modals)
- **Services**: Business logic and API communication
- **Guards**: Route protection and authentication
- **Interceptors**: HTTP request/response handling
## Style Guide
### Color Palette

View file

@ -21,8 +21,7 @@
{
"glob": "**/*",
"input": "public"
},
"src/assets"
}
],
"styles": [
"src/styles.css"

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test';
test('backend works', async ({ page }) => {
await page.goto('http://localhost:8080/health');
const response = await page.textContent('body');
expect(response).toBeTruthy();
expect(page.getByText('{"status":"UP"}')).toBeVisible();
});

View file

@ -1,36 +0,0 @@
import { test, expect } from '@playwright/test';
test('home page loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Casino/);
await expect(page.getByRole('heading', { name: 'Willkommensbonus' })).toBeVisible();
await expect(page.getByText('von bis zu €')).toBeVisible();
});
test('registration popup should open and close', async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
await expect(page.getByText('Konto erstellenE-')).toBeVisible();
await page.getByRole('button', { name: 'Dialog schließen' }).click();
await expect(page.getByText('Konto erstellenE-')).not.toBeVisible();
});
test('registration should work', async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
await page.getByRole('textbox', { name: 'E-Mail' }).fill('test@kjan.email');
await page.getByRole('textbox', { name: 'Benutzername' }).fill('test-playwright');
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
await page.locator('form').getByRole('button', { name: 'Registrieren' }).click();
await page.getByRole('button', { name: 'Dialog schließen' }).click();
await page.getByRole('navigation').getByRole('button', { name: 'Anmelden' }).click();
await page.getByRole('textbox', { name: 'Benutzername oder E-Mail' }).fill('test@kjan.email');
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
await page.locator('form').getByRole('button', { name: 'Anmelden' }).click();
await expect(page.getByText('Email not verified')).toBeVisible();
});

View file

@ -14,16 +14,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.0",
"@angular/cdk": "~20.0.0",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/router": "^20.0.0",
"@fortawesome/angular-fontawesome": "^2.0.0",
"@angular/animations": "^19.0.0",
"@angular/cdk": "~19.2.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.2.4",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@fortawesome/angular-fontawesome": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
@ -39,14 +39,13 @@
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@playwright/test": "^1.52.0",
"@angular-devkit/build-angular": "^19.0.0",
"@angular/cli": "^19.2.5",
"@angular/compiler-cli": "^19.0.0",
"@types/jasmine": "~5.1.0",
"angular-eslint": "20.0.0",
"eslint": "^9.28.0",
"jasmine-core": "~5.8.0",
"angular-eslint": "19.4.0",
"eslint": "^9.25.1",
"jasmine-core": "~5.7.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
@ -54,6 +53,6 @@
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.4.2",
"typescript": "~5.8.0",
"typescript-eslint": "8.34.0"
"typescript-eslint": "8.32.1"
}
}

View file

@ -1,49 +0,0 @@
// playwright.config.ts (or .js)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
// This baseURL is for your frontend tests.
// Tests hitting the backend directly will use absolute URLs.
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: {
command:
'cd .. && conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"',
// **IMPORTANT CHANGE HERE:**
// Point to your backend's health check endpoint.
// If your Spring Boot app uses Actuator, it might be /actuator/health
// Verify the correct health endpoint for your backend.
url: 'http://localhost:8080/health', // Or "http://localhost:8080/actuator/health"
reuseExistingServer: !process.env.CI,
// **INCREASE TIMEOUT SIGNIFICANTLY**
// Gradle + Spring Boot can take a while, especially on first run or in CI.
// Adjust as needed, e.g., 3-5 minutes.
timeout: 300 * 1000, // 300 seconds = 5 minutes
stdout: 'pipe', // Good for capturing logs in CI reports
stderr: 'pipe',
// Optional: If your server needs specific environment variables
// env: {
// SPRING_PROFILES_ACTIVE: 'test', // Example for Spring Boot
// },
},
});

View file

@ -1,4 +1,4 @@
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
@ -12,7 +12,7 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes),
FontAwesomeModule,
provideHttpClient(withInterceptors([httpInterceptor])),
provideZonelessChangeDetection(),
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
],
};

View file

@ -62,12 +62,14 @@ export const routes: Routes = [
loadComponent: () =>
import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'),
canActivate: [authGuard],
},
{
path: 'lootboxes/open/:id',
loadComponent: () =>
import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'),
canActivate: [authGuard],
children: [
{
path: 'open/:id',
loadComponent: () =>
import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'),
canActivate: [authGuard],
},
],
},
{
path: 'dice',

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, inject } from '@angular/core';
import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { LoginRequest } from '../../../model/auth/LoginRequest';
@ -20,11 +20,11 @@ export class LoginComponent {
@Output() closeDialog = new EventEmitter<void>();
@Output() forgotPassword = new EventEmitter<void>();
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);
constructor() {
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router
) {
this.loginForm = this.fb.group({
usernameOrEmail: ['', [Validators.required]],
password: ['', [Validators.required]],

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, OnInit, inject } from '@angular/core';
import { Component, EventEmitter, Output, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@ -22,12 +22,12 @@ export default class RecoverPasswordComponent implements OnInit {
@Output() closeDialog = new EventEmitter<void>();
@Output() switchToLogin = new EventEmitter<void>();
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);
private route = inject(ActivatedRoute);
constructor() {
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
private route: ActivatedRoute
) {
this.emailForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, inject } from '@angular/core';
import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RegisterRequest } from '../../../model/auth/RegisterRequest';
import { AuthService } from '@service/auth.service';
@ -19,10 +19,10 @@ export class RegisterComponent {
@Output() switchForm = new EventEmitter<void>();
@Output() closeDialog = new EventEmitter<void>();
private fb = inject(FormBuilder);
private authService = inject(AuthService);
constructor() {
constructor(
private fb: FormBuilder,
private authService: AuthService
) {
this.registerForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
username: ['', [Validators.required, Validators.minLength(3)]],

View file

@ -61,9 +61,6 @@ export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
startVal: this.previousValue,
duration: this.duration,
decimalPlaces: 2,
useEasing: true,
useGrouping: false,
easingFn: (t, b, c, d) => {
if (this.ease === 'power1.out') {
return c * (1 - Math.pow(1 - t / d, 1)) + b;

View file

@ -1,11 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
inject,
} from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Card } from '@blackjack/models/blackjack.model';
import { PlayingCardComponent } from '../playing-card/playing-card.component';
@ -54,7 +47,7 @@ export class DealerHandComponent implements OnChanges {
private lastCardCount = 0;
protected gameControlsService = inject(GameControlsService);
constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) {

View file

@ -1,11 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
inject,
} from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GameState } from '@blackjack/enum/gameState';
import { Card } from '@blackjack/models/blackjack.model';
@ -76,7 +69,7 @@ export class GameControlsComponent {
protected readonly GameState = GameState;
protected gameControlsService = inject(GameControlsService);
constructor(protected gameControlsService: GameControlsService) {}
get canDoubleDown(): boolean {
return (

View file

@ -7,7 +7,6 @@ import {
Output,
signal,
SimpleChanges,
inject,
} from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
@ -122,9 +121,7 @@ export class GameInfoComponent implements OnChanges {
betForm: FormGroup;
private bettingService = inject(BettingService);
constructor() {
constructor(private bettingService: BettingService) {
this.betForm = this.bettingService.createBetForm();
}

View file

@ -1,11 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
inject,
} from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from '../playing-card/playing-card.component';
import { Card } from '@blackjack/models/blackjack.model';
@ -56,7 +49,7 @@ export class PlayerHandComponent implements OnChanges {
private lastCardCount = 0;
protected gameControlsService = inject(GameControlsService);
constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) {

View file

@ -6,7 +6,6 @@ import {
Input,
OnChanges,
SimpleChanges,
inject,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { gsap } from 'gsap';
@ -59,7 +58,7 @@ export class PlayingCardComponent implements AfterViewInit, OnChanges {
@Input({ required: true }) hidden!: boolean;
@Input() isNew = false;
private elementRef = inject(ElementRef);
constructor(private elementRef: ElementRef) {}
get isRedSuit(): boolean {
return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';

View file

@ -1,11 +1,11 @@
import { Injectable, inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Injectable({
providedIn: 'root',
})
export class BettingService {
private fb = inject(FormBuilder);
constructor(private fb: FormBuilder) {}
createBetForm(): FormGroup {
return this.fb.group({

View file

@ -5,11 +5,13 @@
@if (gameResult()) {
<div class="mb-6 text-center result-text">
<h2 class="text-2xl font-bold mb-2" [class]="getResultClass()">
{{ gameResult()?.isWin ? 'Du hast gewonnen!' : 'Du hast verloren' }}
{{ gameResult()?.isWin ? 'You Won!' : 'You Lost' }}
</h2>
<p class="text-lg">
Münze zeigt:
<span class="font-bold">{{ gameResult()?.coinSide === 'HEAD' ? 'KOPF' : 'ZAHL' }}</span>
Coin landed on:
<span class="font-bold">{{
gameResult()?.coinSide === 'HEAD' ? 'HEAD' : 'TAILS'
}}</span>
</p>
@if (gameResult()?.isWin) {
<p class="text-xl mt-2">
@ -33,7 +35,7 @@
<div
class="front coin-side bg-yellow-500 flex items-center justify-center text-2xl font-bold"
>
<div class="coin-text">KOPF</div>
<div class="coin-text">HEAD</div>
</div>
<!-- Tails side with non-mirrored text -->
@ -41,7 +43,7 @@
class="back coin-side bg-gray-700 flex items-center justify-center text-2xl font-bold text-white"
>
<!-- Using direct inline transform to counter the mirroring effect -->
<span style="display: inline-block; transform: scaleX(1)">ZAHL</span>
<span style="display: inline-block; transform: scaleX(1)">TAILS</span>
</div>
</div>
</div>
@ -54,7 +56,7 @@
class="button-primary py-3 px-6 relative text-lg"
[class.opacity-50]="gameInProgress()"
>
Auf ZAHL setzen
Bet TAILS
</button>
<button
(click)="betHeads()"
@ -62,7 +64,7 @@
class="button-primary py-3 px-6 relative text-lg"
[class.opacity-50]="gameInProgress()"
>
Auf KOPF setzen
Bet HEAD
</button>
</div>
</div>
@ -70,11 +72,11 @@
<!-- Game information panel -->
<div class="col-span-1">
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spielinformationen</h3>
<h3 class="section-heading text-xl mb-4">Game Information</h3>
<div class="space-y-4">
<!-- Current bet display -->
<div class="flex justify-between items-center">
<span class="text-text-secondary">Aktueller Einsatz:</span>
<span class="text-text-secondary">Current Bet:</span>
<span [class]="currentBet() > 0 ? 'text-accent-red' : 'text-text-secondary'">
<app-animated-number [value]="currentBet()" [duration]="0.5"></app-animated-number>
</span>
@ -82,7 +84,7 @@
<!-- Available balance -->
<div class="flex justify-between items-center">
<span class="text-text-secondary">Dein Guthaben:</span>
<span class="text-text-secondary">Your Balance:</span>
<span class="text-white">
{{ balance() | currency: 'EUR' }}
</span>
@ -101,9 +103,9 @@
<!-- Custom bet input -->
<div class="space-y-1">
<div class="flex justify-between">
<label for="bet" class="text-sm text-text-secondary">Einsatzbetrag</label>
<label for="bet" class="text-sm text-text-secondary">Bet Amount</label>
<span *ngIf="isInvalidBet()" class="text-xs text-accent-red animate-pulse"
>Darf Guthaben nicht überschreiten</span
>Cannot exceed balance</span
>
</div>
<input
@ -127,11 +129,11 @@
<!-- Rules/info section -->
<div class="mt-6 pt-4 border-t border-gray-700">
<h4 class="text-lg font-semibold mb-2">Spielregeln</h4>
<h4 class="text-lg font-semibold mb-2">How to Play</h4>
<ul class="text-sm text-text-secondary space-y-1">
<li>Wähle deinen Einsatzbetrag</li>
<li>Wähle Kopf oder Zahl</li>
<li>Gewinne das Doppelte deines Einsatzes bei richtiger Wahl</li>
<li>Choose your bet amount</li>
<li>Select Heads or Tails</li>
<li>Win double your bet if correct</li>
</ul>
</div>
</div>

View file

@ -44,14 +44,14 @@ export default class CoinflipComponent implements OnInit {
private coinflipSound?: HTMLAudioElement;
ngOnInit(): void {
// Abonniere Benutzerupdates für Echtzeitaktualisierungen des Guthabens
// Subscribe to user updates for real-time balance changes
this.authService.userSubject.subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
// Initialisiere Münzwurf-Sound
// Initialize coinflip sound
this.coinflipSound = new Audio('/sounds/coinflip.mp3');
}
@ -65,26 +65,26 @@ export default class CoinflipComponent implements OnInit {
const inputElement = event.target as HTMLInputElement;
let value = Number(inputElement.value);
// Setze ungültigen Einsatz-Status zurück
// Reset invalid bet state
this.isInvalidBet.set(false);
// Erzwinge Mindesteinsatz von 1
// Enforce minimum bet of 1
if (value <= 0) {
value = 1;
}
// Begrenze Einsatz auf verfügbares Guthaben und zeige Feedback
// Cap bet at available balance and show feedback
if (value > this.balance()) {
value = this.balance();
// Visuelles Feedback anzeigen
// Show visual feedback
this.isInvalidBet.set(true);
// Zeige den Fehler kurz an
// Indicate the error briefly
setTimeout(() => this.isInvalidBet.set(false), 800);
// Aktualisiere das Eingabefeld direkt, um dem Benutzer den maximalen Wert anzuzeigen
// Update the input field directly to show the user the max value
inputElement.value = String(value);
}
// Aktualisiere Signale
// Update signals
this.betInputValue.set(value);
this.currentBet.set(value);
}
@ -100,34 +100,34 @@ export default class CoinflipComponent implements OnInit {
private placeBet(side: 'HEAD' | 'TAILS') {
if (this.gameInProgress() || this.isActionInProgress()) return;
// Setze vorheriges Ergebnis zurück
// Reset previous result
this.gameResult.set(null);
this.errorMessage.set('');
// Setze Spielstatus
// Set game state
this.gameInProgress.set(true);
this.isActionInProgress.set(true);
// Spiele Einsatz-Sound
// Play bet sound
this.audioService.playBetSound();
// Erstelle Einsatz-Anfrage
// Create bet request
const request: CoinflipRequest = {
betAmount: this.currentBet(),
coinSide: side,
};
// API aufrufen
// Call API
this.http
.post<CoinflipGame>('/backend/coinflip', request)
.pipe(
catchError((error) => {
console.error('Fehler beim Spielen von Coinflip:', error);
console.error('Error playing coinflip:', error);
if (error.status === 400 && error.error.message.includes('insufficient')) {
this.errorMessage.set('Unzureichendes Guthaben');
this.errorMessage.set('Insufficient funds');
} else {
this.errorMessage.set('Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
this.errorMessage.set('An error occurred. Please try again.');
}
this.gameInProgress.set(false);
@ -140,37 +140,37 @@ export default class CoinflipComponent implements OnInit {
.subscribe((result) => {
if (!result) return;
console.log('API-Antwort:', result);
console.log('API response:', result);
// Behebe mögliche Inkonsistenzen bei der Eigenschaftenbenennung vom Backend
// Fix potential property naming inconsistency from the backend
const fixedResult: CoinflipGame = {
isWin: result.isWin ?? result.win,
payout: result.payout,
coinSide: result.coinSide,
};
console.log('Korrigiertes Ergebnis:', fixedResult);
console.log('Fixed result:', fixedResult);
// Spiele Münzwurf-Animation und -Sound
// Play coin flip animation and sound
this.playCoinFlipAnimation(fixedResult.coinSide);
// Setze Ergebnis nach Abschluss der Animation
// Set result after animation completes
setTimeout(() => {
this.gameResult.set(fixedResult);
// Aktualisiere Guthaben mit neuem Wert vom Auth-Service
// Update balance with new value from auth service
this.authService.loadCurrentUser();
// Spiele Gewinn-Sound, wenn der Spieler gewonnen hat
// Play win sound if player won
if (fixedResult.isWin) {
this.audioService.playWinSound();
}
// Setze Spielstatus nach Anzeigen des Ergebnisses zurück
// Reset game state after showing result
setTimeout(() => {
this.gameInProgress.set(false);
}, 1500);
}, 1100); // Kurz nach Ende der Animation
}, 1100); // Just after animation ends
});
}
@ -179,50 +179,48 @@ export default class CoinflipComponent implements OnInit {
const coinEl = this.coinElement.nativeElement;
// Setze bestehende Animationen zurück
// Reset any existing animations
coinEl.classList.remove('animate-to-heads', 'animate-to-tails');
// Setze alle Inline-Styles von vorherigen Animationen zurück
// Reset any inline styles from previous animations
coinEl.style.transform = '';
// Erzwinge Reflow, um Animation neu zu starten
// Force a reflow to restart animation
void coinEl.offsetWidth;
// Spiele Münzwurf-Sound
// Play flip sound
if (this.coinflipSound) {
this.coinflipSound.currentTime = 0;
this.coinflipSound
.play()
.catch((err) => console.error('Fehler beim Abspielen des Sounds:', err));
this.coinflipSound.play().catch((err) => console.error('Error playing sound:', err));
}
// Füge passende Animationsklasse basierend auf dem Ergebnis hinzu
// Add appropriate animation class based on result
if (result === 'HEAD') {
coinEl.classList.add('animate-to-heads');
} else {
coinEl.classList.add('animate-to-tails');
}
console.log(`Animation angewendet für Ergebnis: ${result}`);
console.log(`Animation applied for result: ${result}`);
}
/**
* Validiert Eingabe während der Benutzer tippt, um ungültige Werte zu verhindern
* Validates input as the user types to prevent invalid values
*/
validateBetInput(event: KeyboardEvent) {
// Erlaube Navigationstasten (Pfeile, Entf, Rücktaste, Tab)
// Allow navigation keys (arrows, delete, backspace, tab)
const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab'];
if (navigationKeys.includes(event.key)) {
return;
}
// Erlaube nur Zahlen
// Only allow numbers
if (!/^\d$/.test(event.key)) {
event.preventDefault();
return;
}
// Ermittle den Wert, der nach dem Tastendruck entstehen würde
// Get the value that would result after the keypress
const input = event.target as HTMLInputElement;
const currentValue = input.value;
const cursorPosition = input.selectionStart || 0;
@ -232,14 +230,14 @@ export default class CoinflipComponent implements OnInit {
currentValue.substring(input.selectionEnd || cursorPosition);
const numValue = Number(newValue);
// Verhindere Werte, die größer als das Guthaben sind
// Prevent values greater than balance
if (numValue > this.balance()) {
event.preventDefault();
}
}
// Der Paste-Handler wurde der Einfachheit halber entfernt, da die updateBet-Methode
// jeden Wert behandelt, der in das Eingabefeld gelangt
// We removed the paste handler for simplicity since the updateBet method
// will handle any value that gets into the input field
getResultClass() {
if (!this.gameResult()) return '';

View file

@ -1,305 +1,121 @@
<div class="container mx-auto px-4 py-6 space-y-8">
<h1 class="text-3xl font-bold text-white mb-6">Dice</h1>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-3 space-y-6 flex flex-col gap-4">
<form [formGroup]="diceForm">
<div class="card p-6">
<div class="space-y-4">
<div class="flex justify-between items-center mb-2">
<h3 class="text-white font-semibold">
Zielwert:
<span class="text-white">{{
diceForm.get('targetValue')?.value | number: '1.0-2'
}}</span>
</h3>
</div>
<div class="container mx-auto px-4 py-8 space-y-8">
<h1 class="text-3xl font-bold text-white mb-6">Dice Game</h1>
<div class="relative py-4">
<div class="flex justify-between text-xs text-text-secondary px-1 mb-2">
<span>0</span>
<span>25</span>
<span>50</span>
<span>75</span>
<span>100</span>
</div>
<style>
@keyframes fade-out {
from {
opacity: 0.4;
}
to {
opacity: 0;
}
}
.result-marker {
transition: left 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.win-display {
transition: all 0.5s ease;
}
</style>
<div class="relative h-12 bg-deep-blue-dark rounded-xl overflow-hidden shadow-inner">
<div
class="absolute top-0 left-0 w-full h-full transition-all duration-300 ease-in-out"
[style.background]="getTrackGradient()"
></div>
<input
id="targetValue"
type="range"
formControlName="targetValue"
min="1"
max="99"
step="1"
class="w-full h-full absolute top-0 left-0 opacity-0 cursor-pointer"
[appDragSound]="diceForm.get('targetValue')"
/>
<div
class="absolute top-1/2 -translate-y-1/2 w-10 h-10 bg-white rounded-full flex items-center justify-center shadow-lg pointer-events-none border-2 ease-in-out"
[ngClass]="{
'border-emerald': diceForm.get('rollOver')?.value,
'border-accent-red': !diceForm.get('rollOver')?.value,
}"
[style.left]="'calc(' + (diceForm.get('targetValue')?.value ?? 50) + '% - 20px)'"
>
<div
class="absolute -top-12 left-1/2 -translate-x-1/2 bg-white text-deep-blue-contrast px-3 py-1 rounded-md text-sm font-bold shadow transition-all duration-300 ease-in-out win-display"
>
{{ potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2' }}
</div>
<span class="text-deep-blue-contrast text-sm font-extrabold select-none">
{{ diceForm.get('rollOver')?.value ? '>' : '<' }}
</span>
</div>
<div class="hidden">
<div class="relative flex items-center justify-center">
<svg width="60" height="60" viewBox="0 0 100 100" class="filter drop-shadow-md">
<polygon points="50,0 100,25 100,75 50,100 0,75 0,25" fill="white" />
</svg>
<span
class="absolute text-deep-blue-contrast font-bold"
style="font-size: 14px"
>
{{ potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2' }}
</span>
</div>
</div>
@if (rolledValue() !== null) {
<div
class="absolute top-0 h-full z-20 result-marker"
[style.left]="rolledValue() + '%'"
style="transition: left 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55)"
>
<div
class="h-full w-2 pointer-events-none animate-pulse"
[ngClass]="{
'bg-emerald': win(),
'bg-accent-red': !win(),
}"
></div>
</div>
}
</div>
<div class="relative h-1 mt-1">
@for (i of [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; track i) {
<div
class="absolute top-0 w-0.5 h-1 bg-text-tertiary"
[style.left]="i + '%'"
></div>
}
</div>
</div>
@if (
hasError('targetValue', 'required') ||
hasError('targetValue', 'min') ||
hasError('targetValue', 'max')
) {
<div class="p-2 bg-accent-red/10 border border-accent-red/20 rounded-lg">
@if (hasError('targetValue', 'required')) {
<span class="text-accent-red text-sm block">Zielwert ist erforderlich</span>
}
@if (hasError('targetValue', 'min')) {
<span class="text-accent-red text-sm block">Zielwert muss mindestens 1 sein</span>
}
@if (hasError('targetValue', 'max')) {
<span class="text-accent-red text-sm block">Zielwert darf höchstens 99 sein</span>
}
</div>
}
</div>
<div class="flex justify-center gap-4 mt-8">
<button
type="button"
(click)="toggleRollMode()"
[ngClass]="{
'bg-emerald text-white': diceForm.get('rollOver')?.value,
'bg-deep-blue-light text-text-secondary hover:bg-deep-blue-light/80':
!diceForm.get('rollOver')?.value,
}"
class="py-3 px-8 rounded-lg text-lg"
appPlaySound
>
Über Zielwert
</button>
<button
type="button"
(click)="toggleRollMode()"
[ngClass]="{
'bg-emerald text-white': !diceForm.get('rollOver')?.value,
'bg-deep-blue-light text-text-secondary hover:bg-deep-blue-light/80':
diceForm.get('rollOver')?.value,
}"
class="py-3 px-8 rounded-lg text-lg"
appPlaySound
>
Unter Zielwert
</button>
</div>
</div>
</form>
@if (rolledValue() !== null) {
<div class="card p-4">
<div class="flex items-center justify-center">
@if (win()) {
<svg
class="w-6 h-6 text-emerald mr-2"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
></path>
</svg>
<p class="text-emerald text-base font-semibold">
Du hast gewonnen! Auszahlung: {{ payout() | currency: 'EUR' : 'symbol' : '1.2-2' }}
</p>
} @else {
<svg
class="w-6 h-6 text-accent-red mr-2"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
></path>
</svg>
<p class="text-accent-red text-base font-semibold">Du hast verloren.</p>
}
</div>
</div>
}
</div>
<div class="lg:col-span-1 space-y-6">
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spielinformationen</h3>
<form [formGroup]="diceForm" (ngSubmit)="roll()" class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-text-secondary">Möglicher Gewinn:</span>
<span class="text-emerald">{{
potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2'
}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-text-secondary">Gewinnchance:</span>
<span class="text-white">{{ winChance() | number: '1.0-2' }}%</span>
</div>
<div class="grid grid-cols-2 gap-2 mb-4">
<button
type="button"
(click)="setBetAmount(0.1)"
class="button-primary py-2 text-sm"
appPlaySound
>
10%
</button>
<button
type="button"
(click)="setBetAmount(0.25)"
class="button-primary py-2 text-sm"
appPlaySound
>
25%
</button>
<button
type="button"
(click)="setBetAmount(0.5)"
class="button-primary py-2 text-sm"
appPlaySound
>
50%
</button>
<button
type="button"
(click)="setBetAmount(1)"
class="button-primary py-2 text-sm"
appPlaySound
>
100%
</button>
</div>
<div class="space-y-1">
<div class="flex justify-between">
<label for="betAmount" class="text-sm text-text-secondary">Einsatzbetrag</label>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<div class="lg:col-span-1 card p-8 space-y-6">
<form [formGroup]="diceForm" (ngSubmit)="roll()" class="space-y-6">
<div class="controls space-y-4">
<div>
<label for="betAmount" class="block text-text-secondary mb-2">Bet Amount:</label>
<input
id="betAmount"
type="number"
formControlName="betAmount"
min="0.01"
step="0.01"
class="w-full px-3 py-2 bg-deep-blue-light text-white rounded focus:outline-none focus:ring-2 ring-emerald"
class="w-full bg-deep-blue-light text-white rounded-lg p-2 focus:outline-none focus:ring-1 focus:ring-emerald"
/>
@if (hasError('betAmount', 'required')) {
<span class="text-accent-red text-xs mt-1 block">Einsatz ist erforderlich</span>
<span class="text-accent-red text-sm mt-1 block">Bet Amount is required</span>
}
@if (hasError('betAmount', 'min')) {
<span class="text-accent-red text-xs mt-1 block"
>Einsatz muss mindestens 0.01 sein</span
<span class="text-accent-red text-sm mt-1 block"
>Bet Amount must be at least 0.01</span
>
}
</div>
<button
type="submit"
class="button-primary w-full py-3 font-bold flex items-center justify-center"
appPlaySound
>
Würfeln
</button>
<div class="mt-6 pt-4 border-t border-gray-700">
<h4 class="text-lg font-semibold mb-2">Spielanleitung</h4>
<ul class="text-sm text-text-secondary space-y-1">
<li>• Setze deinen Einsatz und Zielwert</li>
<li>• Wähle "Über Zielwert" oder "Unter Zielwert"</li>
<li>• Gewinne, wenn der Würfel zu deinen Gunsten fällt</li>
<li>• Höheres Risiko = höhere Belohnung</li>
</ul>
<div>
<div class="block text-text-secondary mb-2">Roll Mode:</div>
<div class="roll-mode flex rounded-lg overflow-hidden">
<button
type="button"
(click)="toggleRollMode()"
[ngClass]="{
'bg-emerald text-white': diceForm.get('rollOver')?.value,
'bg-deep-blue-light text-text-secondary': !diceForm.get('rollOver')?.value,
}"
class="flex-1 py-2 text-center font-semibold transition-colors duration-200"
>
Roll Over
</button>
<button
type="button"
(click)="toggleRollMode()"
[ngClass]="{
'bg-emerald text-white': !diceForm.get('rollOver')?.value,
'bg-deep-blue-light text-text-secondary': diceForm.get('rollOver')?.value,
}"
class="flex-1 py-2 text-center font-semibold transition-colors duration-200"
>
Roll Under
</button>
</div>
</div>
</form>
</div>
<div>
<label for="targetValue" class="block text-text-secondary mb-2"
>Target Value: {{ diceForm.get('targetValue')?.value | number: '1.0-2' }}</label
>
<input
id="targetValue"
type="range"
formControlName="targetValue"
min="1"
max="100"
step="0.01"
class="w-full h-2 bg-deep-blue-light rounded-lg appearance-none cursor-pointer range-lg accent-emerald"
/>
@if (hasError('targetValue', 'required')) {
<span class="text-accent-red text-sm mt-1 block">Target Value is required</span>
}
@if (hasError('targetValue', 'min')) {
<span class="text-accent-red text-sm mt-1 block"
>Target Value must be at least 1</span
>
}
@if (hasError('targetValue', 'max')) {
<span class="text-accent-red text-sm mt-1 block"
>Target Value must be at most 100</span
>
}
</div>
</div>
<div class="info space-y-2 text-text-secondary">
<p>
Win Chance: <span class="text-white">{{ winChance() | number: '1.0-2' }}%</span>
</p>
<p>
Potential Win:
<span class="text-white">{{
potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2'
}}</span>
</p>
</div>
<button type="submit" class="button-primary w-full py-2 font-bold">Roll Dice</button>
</form>
</div>
<div class="lg:col-span-3 card p-8 flex items-center justify-center">
@if (rolledValue() !== null) {
<div class="text-white text-center text-8xl font-bold">
{{ rolledValue() }}
</div>
}
</div>
</div>
@if (rolledValue() !== null) {
<div class="result max-w-sm mx-auto card p-6 mt-8 text-center">
@if (win()) {
<p class="text-emerald text-base font-semibold">
You Won! Payout: {{ payout() | currency: 'EUR' : 'symbol' : '1.2-2' }}
</p>
} @else {
<p class="text-accent-red text-base font-semibold">You Lost.</p>
}
</div>
}
</div>

View file

@ -11,9 +11,6 @@ import { DiceService } from './dice.service';
import { DiceDto, DiceResult } from './dice.model';
import { tap } from 'rxjs/operators';
import { UserService } from '@service/user.service';
import { PlaySoundDirective } from '@shared/directives/play-sound.directive';
import { DragSoundDirective } from '@shared/directives/drag-sound.directive';
import { AudioService } from '@shared/services/audio.service';
type DiceFormGroup = FormGroup<{
betAmount: FormControl<number | null>;
@ -24,14 +21,13 @@ type DiceFormGroup = FormGroup<{
@Component({
selector: 'app-dice',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, PlaySoundDirective, DragSoundDirective],
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './dice.component.html',
})
export default class DiceComponent implements OnInit {
private readonly formBuilder = inject(FormBuilder);
private readonly diceService = inject(DiceService);
private readonly userService = inject(UserService);
private readonly audioService = inject(AudioService);
rolledValue = signal<number | null>(null);
win = signal<boolean | null>(null);
@ -53,23 +49,23 @@ export default class DiceComponent implements OnInit {
createDiceForm(): DiceFormGroup {
return this.formBuilder.group({
betAmount: new FormControl<number | null>(1, {
validators: [Validators.required, Validators.min(1)],
betAmount: new FormControl<number | null>(1.0, {
validators: [Validators.required, Validators.min(0.01)],
nonNullable: true,
}),
rollOver: new FormControl<boolean>(true, {
validators: [Validators.required],
nonNullable: true,
}),
targetValue: new FormControl<number | null>(50, {
validators: [Validators.required, Validators.min(1), Validators.max(99)],
targetValue: new FormControl<number | null>(50.5, {
validators: [Validators.required, Validators.min(1), Validators.max(100)],
nonNullable: true,
}),
});
}
toggleRollMode(): void {
const currentMode = this.diceForm.get('rollOver')?.value ?? true;
const currentMode = this.diceForm.get('rollOver')?.value;
this.diceForm.get('rollOver')?.setValue(!currentMode);
}
@ -108,11 +104,6 @@ export default class DiceComponent implements OnInit {
this.rolledValue.set(result.rolledValue);
this.win.set(result.win);
this.payout.set(result.payout);
if (result.win) {
this.audioService.playWinSound();
}
this.userService.refreshCurrentUser();
},
error: (error) => {
@ -121,29 +112,6 @@ export default class DiceComponent implements OnInit {
});
}
setBetAmount(percentage: number): void {
const user = this.userService['authService'].currentUserValue;
if (!user) return;
const balance = user.balance || 0;
const newBet = Math.max(1, Math.floor(balance * percentage * 100) / 100);
this.diceForm.get('betAmount')?.setValue(newBet);
this.calculateWinChanceAndPotentialWin();
}
getTrackGradient(): string {
const targetValue = this.diceForm.get('targetValue')?.value ?? 50;
const isRollOver = this.diceForm.get('rollOver')?.value ?? true;
if (isRollOver) {
return `linear-gradient(to right, var(--color-accent-red) ${targetValue}%, var(--color-emerald) ${targetValue}%)`;
} else {
return `linear-gradient(to right, var(--color-accent-red) ${targetValue}%, var(--color-emerald) ${targetValue}%)`;
}
}
hasError(controlName: string, errorName: string): boolean {
const control = this.diceForm.get(controlName);
return control !== null && control.touched && control.hasError(errorName);

View file

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { DiceDto, DiceResult } from './dice.model';
@ -10,7 +10,7 @@ import { environment } from '@environments/environment';
export class DiceService {
private apiUrl = `${environment.apiUrl}/dice`;
private http = inject(HttpClient);
constructor(private http: HttpClient) {}
rollDice(diceDto: DiceDto): Observable<DiceResult> {
return this.http.post<DiceResult>(this.apiUrl, diceDto);

View file

@ -4,7 +4,7 @@
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-4">
<div class="lg:col-span-3">
<div class="flex justify-between items-center mb-6">
<h3 class="section-heading text-2xl">Alle Spiele</h3>
<div class="flex space-x-2">
@ -18,54 +18,24 @@
</div>
<div class="slider-container">
<div class="min-w-full space-y-4">
<!-- Top row with 3 games -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card group" *ngFor="let game of featuredGames.slice(0, 3)">
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div class="slider-grid">
<div class="card group" *ngFor="let game of featuredGames">
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<div
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom row with 2 games centered -->
<div
class="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto xl:max-w-3xl xl:gap-6"
>
<div class="card group" *ngFor="let game of featuredGames.slice(3, 5)">
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
>
<div
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button>
</div>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button>
</div>
</div>
</div>
@ -73,5 +43,53 @@
</div>
</div>
</div>
<div class="lg:col-span-1 space-y-6">
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Konto</h3>
<div class="space-y-4">
<button class="button-primary w-full py-2" (click)="openDepositModal()">Einzahlen</button>
<app-deposit
[isOpen]="isDepositModalOpen"
(closeModalEmitter)="closeDepositModal()"
></app-deposit>
<button
class="bg-deep-blue-light hover:bg-deep-blue-contrast w-full py-2 rounded"
(click)="openTransactionModal()"
>
Transaktionen
</button>
<app-transaction-history
[isOpen]="isTransactionModalOpen"
(closeEventEmitter)="closeTransactionModal()"
/>
</div>
</div>
<app-confirmation
[successful]="isDepositSuccessful"
(closeConfirmation)="closeDepositConfirmationModal()"
></app-confirmation>
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Letzte Transaktionen</h3>
<div class="space-y-3">
<div
class="flex justify-between items-center"
*ngFor="let transaction of (recentTransactionData | async)?.transactions"
>
<div>
<p class="text-sm font-medium">{{ transaction.status }}</p>
<p class="text-xs text-text-secondary">
{{ transaction.createdAt | date: 'd.m.Y H:m' }}
</p>
</div>
<span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
{{ transaction.amount | currency: 'EUR' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,21 +1,39 @@
import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core';
import { NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { AsyncPipe, CurrencyPipe, DatePipe, NgFor } from '@angular/common';
import { DepositComponent } from '../deposit/deposit.component';
import { ActivatedRoute, Router } from '@angular/router';
import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component';
import { Game } from 'app/model/Game';
import { Observable } from 'rxjs';
import { TransactionService } from '@service/transaction.service';
import format from 'ajv/dist/vocabularies/format';
import { TransactionHistoryComponent } from '../transaction-history/transaction-history.component';
import { TransactionData } from '../../model/TransactionData';
@Component({
selector: 'app-homepage',
standalone: true,
imports: [NgFor],
imports: [
CurrencyPipe,
NgFor,
DepositComponent,
ConfirmationComponent,
AsyncPipe,
DatePipe,
TransactionHistoryComponent,
],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class HomeComponent implements OnInit {
isDepositModalOpen = false;
isDepositSuccessful = false;
isTransactionModalOpen = false;
public route = inject(ActivatedRoute);
public router = inject(Router);
constructor(
public route: ActivatedRoute,
public router: Router
) {}
ngOnInit() {
this.isDepositSuccessful = this.route.snapshot.queryParams['success'] == 'true';
@ -46,6 +64,12 @@ export default class HomeComponent implements OnInit {
image: '/slots.webp',
route: '/game/slots',
},
{
id: '4',
name: 'Plinko',
image: '/plinko.webp',
route: '/game/plinko',
},
{
id: '5',
name: 'Dice',
@ -60,10 +84,35 @@ export default class HomeComponent implements OnInit {
},
];
allGames: Game[] = [...this.featuredGames];
recentTransactionData: Observable<TransactionData> =
inject(TransactionService).getUsersTransactions(5);
openDepositModal() {
this.isDepositModalOpen = true;
}
closeDepositModal() {
this.isDepositModalOpen = false;
}
openDepositConfirmationModal() {
this.isDepositSuccessful = true;
}
openTransactionModal() {
this.isTransactionModalOpen = true;
}
closeDepositConfirmationModal() {
this.isDepositSuccessful = false;
}
closeTransactionModal() {
this.isTransactionModalOpen = false;
}
navigateToGame(route: string) {
this.router.navigate([route]);
}

View file

@ -21,7 +21,13 @@
(click)="showRegisterForm()"
class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"
>
Jetzt registrieren
Konto erstellen
</button>
<button
(click)="showLoginForm()"
class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded"
>
Anmelden
</button>
}
</div>
@ -40,108 +46,69 @@
<div class="game-card-content">
<h3 class="game-heading-sm">Slots</h3>
<p class="game-text">Klassische Spielautomaten</p>
@if (isLoggedIn()) {
<a
routerLink="game/slots"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
<a
routerLink="game/slots"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
<div class="card">
<div class="game-card-content">
<h3 class="game-heading-sm">Plinko</h3>
<p class="game-text">Spannendes Geschicklichkeitsspiel</p>
<a
routerLink="/game/plinko"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading-sm">Blackjack</h3>
<p class="game-text">Klassisches Kartenspiel</p>
@if (isLoggedIn()) {
<a
routerLink="game/blackjack"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading-sm">Coinflip</h3>
<p class="game-text">Münzwurf</p>
@if (isLoggedIn()) {
<a
routerLink="game/coinflip"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
<a
routerLink="game/blackjack"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
</div>
<div class="slider-grid">
<div class="card">
<div class="game-card-content">
<h3 class="game-heading-sm">Poker</h3>
<p class="game-text">Texas Hold'em & mehr</p>
<a
routerLink="/game/poker"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
<div class="card">
<div class="game-card-content">
<h3 class="game-heading-sm">Dice</h3>
<p class="game-text">Würfelspiel</p>
@if (isLoggedIn()) {
<a
routerLink="game/dice"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
<a
routerLink="/game/dice"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading-sm">Lootboxen</h3>
<p class="game-text">Überraschungskisten</p>
@if (isLoggedIn()) {
<a
routerLink="game/lootboxes"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
<a
routerLink="game/lootboxes"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
</div>
@ -209,7 +176,7 @@
<div class="stat-container">
<div class="stat-number">24/7</div>
<div class="stat-text">Support</div>
<div class="stat-text">Support <span class="text-emerald text-xs">*</span></div>
</div>
</div>
</div>

View file

@ -23,14 +23,15 @@ import RecoverPasswordComponent from '../auth/recover-password/recover-password.
})
export class LandingComponent implements OnInit, OnDestroy {
currentSlide = 0;
private autoplayInterval: ReturnType<typeof setInterval> | undefined;
authService: AuthService = inject(AuthService);
route: ActivatedRoute = inject(ActivatedRoute);
showLogin = signal(false);
showRegister = signal(false);
showRecoverPassword = signal(false);
isLoggedIn = signal(this.authService.isLoggedIn());
ngOnInit() {
this.startAutoplay();
document.body.style.overflow = 'auto';
if (this.route.snapshot.queryParamMap.get('login') === 'true') {
this.showLoginForm();
@ -38,6 +39,7 @@ export class LandingComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.stopAutoplay();
document.body.style.overflow = 'auto';
}
@ -71,13 +73,33 @@ export class LandingComponent implements OnInit, OnDestroy {
prevSlide() {
this.currentSlide = this.currentSlide === 0 ? 1 : 0;
this.resetAutoplay();
}
nextSlide() {
this.currentSlide = this.currentSlide === 1 ? 0 : 1;
this.resetAutoplay();
}
goToSlide(index: number) {
this.currentSlide = index;
this.resetAutoplay();
}
private startAutoplay() {
this.autoplayInterval = setInterval(() => {
this.nextSlide();
}, 5000);
}
private stopAutoplay() {
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
}
}
private resetAutoplay() {
this.stopAutoplay();
this.startAutoplay();
}
}

View file

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { ChangeDetectorRef, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { LootboxService } from '../services/lootbox.service';
@ -26,14 +26,14 @@ export default class LootboxOpeningComponent {
currentUser: User | null = null;
private winSound: HTMLAudioElement;
private route = inject(ActivatedRoute);
private router = inject(Router);
private lootboxService = inject(LootboxService);
private userService = inject(UserService);
private authService = inject(AuthService);
private cdr = inject(ChangeDetectorRef);
constructor() {
constructor(
private route: ActivatedRoute,
private router: Router,
private lootboxService: LootboxService,
private userService: UserService,
private authService: AuthService,
private cdr: ChangeDetectorRef
) {
this.winSound = new Audio('/sounds/win.mp3');
this.loadLootbox();
this.authService.userSubject.subscribe((user) => {

View file

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, OnInit, inject } from '@angular/core';
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LootboxService } from '../services/lootbox.service';
import { LootBox } from 'app/model/LootBox';
@ -86,11 +86,13 @@ export default class LootboxSelectionComponent implements OnInit {
},
];
private lootboxService = inject(LootboxService);
private router = inject(Router);
private cdr = inject(ChangeDetectorRef);
private authService = inject(AuthService);
private userService = inject(UserService);
constructor(
private lootboxService: LootboxService,
private router: Router,
private cdr: ChangeDetectorRef,
private authService: AuthService,
private userService: UserService
) {}
ngOnInit(): void {
this.loadLootboxes();

View file

@ -22,7 +22,7 @@
<div class="flex justify-between items-center mb-4">
<div>
<p class="text-sm font-medium">{{ transaction.status }}</p>
<p class="text-xs text-text-secondary">{{ transaction.createdAt | date: 'd.m.y H:m' }}</p>
<p class="text-xs text-text-secondary">{{ transaction.createdAt | date: 'd.m.Y H:m' }}</p>
</div>
<span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
{{ transaction.amount | currency: 'EUR' }}

View file

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router, ActivatedRoute } from '@angular/router';
@ -19,13 +19,13 @@ export class AuthService {
private userUrl = `${environment.apiUrl}/users`;
private oauthUrl = `${environment.apiUrl}/oauth2`;
private http = inject(HttpClient);
private router = inject(Router);
private route = inject(ActivatedRoute);
userSubject: BehaviorSubject<User | null>;
constructor() {
constructor(
private http: HttpClient,
private router: Router,
private route: ActivatedRoute
) {
this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
// Check for token in URL (OAuth callback) on initialization

View file

@ -7,7 +7,6 @@ import {
OnDestroy,
Output,
ViewChild,
inject,
} from '@angular/core';
import { ModalAnimationService } from '@shared/services/modal-animation.service';
import gsap from 'gsap';
@ -24,7 +23,7 @@ export class ConfirmationComponent implements AfterViewInit, OnDestroy {
@ViewChild('modalBg') modalBg!: ElementRef;
@ViewChild('modalCard') modalCard!: ElementRef;
private modalAnimationService = inject(ModalAnimationService);
constructor(private modalAnimationService: ModalAnimationService) {}
ngAfterViewInit() {
if (this.successful) {

View file

@ -5,19 +5,27 @@
<h3 class="footer-heading">Casino Spiele</h3>
<ul class="space-y-3">
<li>
<a routerLink="/game/slots" class="footer-link">Slots</a>
<a routerLink="/games" class="footer-link">Slots</a>
</li>
<li>
<a routerLink="/game/blackjack" class="footer-link">Blackjack</a>
<a routerLink="/games" class="footer-link">Plinko</a>
</li>
<li>
<a routerLink="/game/dice" class="footer-link">Dice</a>
<a routerLink="/games" class="footer-link">Blackjack</a>
</li>
<li>
<a routerLink="/game/coinflip" class="footer-link">Coinflip</a>
<a routerLink="/games" class="footer-link">Poker</a>
</li>
<li>
<a routerLink="/game/lootboxes" class="footer-link">Lootboxen</a>
<a routerLink="/games" class="footer-link">Liars Dice</a>
</li>
</ul>
</div>
<div class="footer-section">
<h3 class="footer-heading">Andere Spiele</h3>
<ul class="space-y-3">
<li>
<a routerLink="/games" class="footer-link">Lootboxen</a>
</li>
</ul>
</div>
@ -49,6 +57,10 @@
<fa-icon [icon]="faGooglePay" class="footer-payment-icon"></fa-icon>
<span class="footer-payment-text">Google Pay</span>
</div>
<div class="footer-payment-method">
<fa-icon [icon]="faApplePay" class="footer-payment-icon"></fa-icon>
<span class="footer-payment-text">Apple Pay</span>
</div>
</div>
</div>
</div>
@ -56,6 +68,10 @@
<div class="mt-12 pt-8 border-t border-deep-blue-light">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="footer-copyright">
<span class="footer-disclaimer">
<span class="text-emerald">*</span> nicht vorhanden.
</span>
<br />
© {{ currentYear }} Trustworthy Casino. Keine Rechte vorbehalten.
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more