Compare commits

..

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

261 changed files with 3959 additions and 13803 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

@ -3,10 +3,6 @@ name: CI
on:
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
changed_files:
name: Get Changed Files
@ -29,32 +25,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"
@ -104,50 +74,18 @@ jobs:
working-directory: ./backend
run: gradle --stop
validate-docker-frontend:
runs-on: ubuntu-latest
name: Docker frontend validation
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: docker/build-push-action@v6
with:
context: frontend/
file: frontend/.docker/Dockerfile
push: false
validate-docker-backend:
runs-on: ubuntu-latest
name: Docker backend validation
needs: changed_files
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: docker/build-push-action@v6
with:
context: backend/
file: backend/.docker/Dockerfile
push: false
eslint:
runs-on: docker
container:
image: git.kjan.de/actions/runner-bun:latest
runs-on: ubuntu-latest
name: eslint
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install bun
uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
working-directory: ./frontend
with:
@ -165,83 +103,18 @@ 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
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: git.kjan.de/actions/runner-bun:latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- uses: actions/cache@v4
working-directory: ./frontend
with:
path: |
frontend/node_modules/
key: ${{ runner.os }}-bun-
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
cd frontend
bun install
- name: Run oxlint
run: |
cd frontend
bun run oxlint
prettier:
runs-on: docker
runs-on: ubuntu-latest
name: prettier
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: git.kjan.de/actions/runner-bun:latest
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install bun
uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
working-directory: ./frontend
with:
@ -260,15 +133,17 @@ jobs:
bun run format:check
test-build:
runs-on: docker
runs-on: ubuntu-latest
name: test-build
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: git.kjan.de/actions/runner-bun:latest
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install bun
uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
working-directory: ./frontend
with:

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 18
- 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.4"
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.0.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,10 @@ 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.4")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.4")
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.springframework.boot:spring-boot-starter-mail")
runtimeOnly("com.h2database:h2")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
}
tasks.withType<Test> {

Binary file not shown.

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.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

10
backend/gradlew vendored
View file

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,7 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -205,7 +203,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@ -213,7 +211,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.

6
backend/gradlew.bat vendored
View file

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -70,11 +68,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View file

@ -1,20 +1,10 @@
package de.szut.casino;
import de.szut.casino.lootboxes.LootBoxEntity;
import de.szut.casino.lootboxes.LootBoxRepository;
import de.szut.casino.lootboxes.RewardEntity;
import de.szut.casino.lootboxes.RewardRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
@SpringBootApplication
public class CasinoApplication {
@ -26,69 +16,4 @@ public class CasinoApplication {
public static RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public static JavaMailSenderImpl javaMailSenderImpl() {
return new JavaMailSenderImpl();
}
@Bean
public CommandLineRunner initData(LootBoxRepository lootBoxRepository, RewardRepository rewardRepository) {
return _ -> {
if (lootBoxRepository.count() == 0) {
LootBoxEntity basicLootBox = new LootBoxEntity();
basicLootBox.setName("Basic LootBox");
basicLootBox.setPrice(new BigDecimal("2"));
basicLootBox.setRewards(new ArrayList<>()); // Initialize the list
LootBoxEntity premiumLootBox = new LootBoxEntity();
premiumLootBox.setName("Premium LootBox");
premiumLootBox.setPrice(new BigDecimal("5"));
premiumLootBox.setRewards(new ArrayList<>()); // Initialize the list
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
RewardEntity commonReward = new RewardEntity();
commonReward.setValue(new BigDecimal("0.50"));
commonReward.setProbability(new BigDecimal("0.7"));
RewardEntity rareReward = new RewardEntity();
rareReward.setValue(new BigDecimal("2.00"));
rareReward.setProbability(new BigDecimal("0.25"));
RewardEntity epicReward = new RewardEntity();
epicReward.setValue(new BigDecimal("5.00"));
epicReward.setProbability(new BigDecimal("0.5"));
RewardEntity premiumCommon = new RewardEntity();
premiumCommon.setValue(new BigDecimal("2.00"));
premiumCommon.setProbability(new BigDecimal("0.6"));
RewardEntity premiumRare = new RewardEntity();
premiumRare.setValue(new BigDecimal("5.00"));
premiumRare.setProbability(new BigDecimal("0.3"));
RewardEntity legendaryReward = new RewardEntity();
legendaryReward.setValue(new BigDecimal("15.00"));
legendaryReward.setProbability(new BigDecimal("0.10"));
rewardRepository.saveAll(Arrays.asList(
commonReward, rareReward, epicReward,
premiumCommon, premiumRare, legendaryReward));
basicLootBox.getRewards().add(commonReward);
basicLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(premiumCommon);
premiumLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(legendaryReward);
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
System.out.println("Initial LootBoxes and rewards created successfully");
} else {
System.out.println("LootBoxes already exist, skipping initialization");
}
};
}
}

View file

@ -1,7 +1,8 @@
package de.szut.casino.blackjack;
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
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;
@ -9,60 +10,122 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
import java.math.BigDecimal;
import java.util.*;
@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);
public ResponseEntity<Object> getGame(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
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);
public ResponseEntity<Object> hit(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
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);
public ResponseEntity<Object> stand(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
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);
public ResponseEntity<Object> doubleDown(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
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, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
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, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, betDto)) {
return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds"));
}
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,25 +25,18 @@ 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());
this.deckService.initializeDeck(game);
this.deckService.dealInitialCards(game);
game.setBet(betAmount);
initializeDeck(game);
dealInitialCards(game);
game.setState(getState(game));
return processGameBasedOnState(game);
deductBetFromBalance(user, betAmount);
return blackJackGameRepository.save(game);
}
@Transactional
@ -65,11 +44,12 @@ public class BlackJackService {
if (game.getState() != BlackJackState.IN_PROGRESS) {
return game;
}
this.deckService.dealCardToPlayer(game);
dealCardToPlayer(game);
updateGameStateAndBalance(game);
return processGameBasedOnState(game);
return blackJackGameRepository.save(game);
}
@Transactional
@ -77,11 +57,11 @@ public class BlackJackService {
if (game.getState() != BlackJackState.IN_PROGRESS) {
return game;
}
dealCardsToDealerUntilMinimumScore(game);
determineWinnerAndUpdateBalance(game);
return processGameBasedOnState(game);
return blackJackGameRepository.save(game);
}
@Transactional
@ -89,43 +69,117 @@ public class BlackJackService {
if (game.getState() != BlackJackState.IN_PROGRESS || game.getPlayerCards().size() != 2) {
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) {
return stand(game);
}
return game;
}
private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS) {
this.blackJackGameRepository.delete(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 blackJackGameRepository.save(game);
}
private void updateGameStateAndBalance(BlackJackGameEntity game) {
game.setState(getState(game));
if (game.getState() == BlackJackState.PLAYER_WON) {
updateUserBalance(game, true);
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
updateUserBalance(game, false);
private BlackJackGameEntity refreshGameState(BlackJackGameEntity game) {
return blackJackGameRepository.findById(game.getId()).orElse(game);
}
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);
}
}
}
private void determineWinnerAndUpdateBalance(BlackJackGameEntity game) {
int playerValue = calculateHandValue(game.getPlayerCards());
int dealerValue = calculateHandValue(game.getDealerCards());
@ -141,27 +195,80 @@ public class BlackJackService {
updateUserBalance(game, false);
}
}
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
user.setBalance(user.getBalance().subtract(betAmount));
userRepository.save(user);
}
protected void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
UserEntity user = game.getUser();
@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 +278,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 +306,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

@ -1,6 +0,0 @@
package de.szut.casino.coinflip;
public enum CoinSide {
HEAD,
TAILS;
}

View file

@ -1,39 +0,0 @@
package de.szut.casino.coinflip;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class CoinflipController {
private final UserService userService;
private final BalanceService balanceService;
private final CoinflipService coinflipService;
public CoinflipController(UserService userService, BalanceService balanceService, CoinflipService coinflipService) {
this.userService = userService;
this.balanceService = balanceService;
this.coinflipService = coinflipService;
}
@PostMapping("/coinflip")
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
UserEntity user = userService.getCurrentUser();
if (!this.balanceService.hasFunds(user, coinflipDto)) {
throw new InsufficientFundsException();
}
return ResponseEntity.ok(coinflipService.play(user, coinflipDto));
}
}

View file

@ -1,23 +0,0 @@
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;
public CoinflipDto(BigDecimal betAmount, CoinSide coinSide) {
super(betAmount);
this.coinSide = coinSide;
}
}

View file

@ -1,16 +0,0 @@
package de.szut.casino.coinflip;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@AllArgsConstructor
@Setter
@Getter
public class CoinflipResult {
private boolean isWin;
private BigDecimal payout;
private CoinSide coinSide;
}

View file

@ -1,35 +0,0 @@
package de.szut.casino.coinflip;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Random;
@Service
public class CoinflipService {
private final Random random;
private final BalanceService balanceService;
public CoinflipService(BalanceService balanceService, Random random) {
this.balanceService = balanceService;
this.random = random;
}
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO, coinSide);
if (coinSide == coinflipDto.getCoinSide()) {
coinflipResult.setWin(true);
BigDecimal payout = coinflipDto.getBetAmount().multiply(BigDecimal.TWO);
this.balanceService.addFunds(user, payout);
coinflipResult.setPayout(payout);
}
return coinflipResult;
}
}

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

@ -1,6 +1,7 @@
package de.szut.casino.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;

View file

@ -1,30 +0,0 @@
package de.szut.casino.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig {
@Value("${app.frontend-host}")
private String frontendHost;
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(frontendHost)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}

View file

@ -7,14 +7,21 @@ import com.stripe.param.checkout.SessionCreateParams;
import de.szut.casino.deposit.dto.AmountDto;
import de.szut.casino.deposit.dto.SessionIdDto;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.dto.KeycloakUserDto;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
@RestController
public class DepositController {
@ -27,18 +34,23 @@ public class DepositController {
private final TransactionService transactionService;
private final UserService userService;
private final RestTemplate restTemplate;
public DepositController(TransactionService transactionService, UserService userService) {
private final UserRepository userRepository;
public DepositController(TransactionService transactionService, RestTemplate restTemplate, UserRepository userRepository) {
this.transactionService = transactionService;
this.userService = userService;
this.restTemplate = restTemplate;
this.userRepository = userRepository;
}
@PostMapping("/deposit/checkout")
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
Stripe.apiKey = stripeKey;
UserEntity user = userService.getCurrentUser();
KeycloakUserDto userData = getAuthentikUserInfo(token);
Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub());
SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder()
@ -51,16 +63,28 @@ public class DepositController {
.build())
.setQuantity(1L)
.build())
.setSuccessUrl(frontendHost + "/home?success=true")
.setCancelUrl(frontendHost + "/home?success=false")
.setSuccessUrl(frontendHost+"/home?success=true")
.setCancelUrl(frontendHost+"/home?success=false")
.setMode(SessionCreateParams.Mode.PAYMENT)
.build();
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()));
}
private KeycloakUserDto getAuthentikUserInfo(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
return response.getBody();
}
}

View file

@ -3,13 +3,10 @@ package de.szut.casino.deposit;
import com.stripe.exception.StripeException;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionRetrieveParams;
import de.szut.casino.security.service.EmailService;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import jakarta.mail.MessagingException;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Optional;
@ -17,12 +14,10 @@ import java.util.Optional;
public class TransactionService {
private final TransactionRepository transactionRepository;
private final UserRepository userRepository;
private final EmailService emailService;
public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository, EmailService emailService) {
public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository) {
this.transactionRepository = transactionRepository;
this.userRepository = userRepository;
this.emailService = emailService;
}
public void createTransaction(
@ -39,7 +34,7 @@ public class TransactionService {
transactionRepository.save(transaction);
}
public void fulfillCheckout(String sessionID) throws StripeException, MessagingException, IOException {
public void fulfillCheckout(String sessionID) throws StripeException {
SessionRetrieveParams params = SessionRetrieveParams.builder()
.addExpand("line_items")
.build();
@ -65,6 +60,5 @@ public class TransactionService {
userRepository.save(user);
transactionRepository.save(transaction);
emailService.sendDepositEmail(transaction);
}
}

View file

@ -6,7 +6,6 @@ import com.stripe.model.Event;
import com.stripe.model.checkout.Session;
import com.stripe.net.Webhook;
import jakarta.annotation.PostConstruct;
import jakarta.mail.MessagingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@ -16,7 +15,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.Objects;
@RestController
@ -40,7 +38,7 @@ public class WebhookController {
}
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException, MessagingException, IOException {
public ResponseEntity<String> webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException {
Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
if (Objects.equals(event.getType(), "checkout.session.completed") || Objects.equals(event.getType(), "checkout.session.async_payment_succeeded")) {

View file

@ -1,38 +0,0 @@
package de.szut.casino.dice;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class DiceController {
private final UserService userService;
private final BalanceService balanceService;
private final DiceService diceService;
public DiceController(UserService userService, BalanceService balanceService, DiceService diceService) {
this.userService = userService;
this.balanceService = balanceService;
this.diceService = diceService;
}
@PostMapping("/dice")
public ResponseEntity<Object> rollDice(@RequestBody @Valid DiceDto diceDto) {
UserEntity user = userService.getCurrentUser();
if (!this.balanceService.hasFunds(user, diceDto)) {
throw new InsufficientFundsException();
}
return ResponseEntity.ok(diceService.play(user, diceDto));
}
}

View file

@ -1,29 +0,0 @@
package de.szut.casino.dice;
import de.szut.casino.shared.dto.BetDto;
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;
@NotNull
@DecimalMin(value = "1.00")
@DecimalMax(value = "100")
private BigDecimal targetValue;
public DiceDto(BigDecimal betAmount, boolean rollOver, BigDecimal targetValue) {
super(betAmount);
this.rollOver = rollOver;
this.targetValue = targetValue;
}
}

View file

@ -1,20 +0,0 @@
package de.szut.casino.dice;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Setter
@Getter
public class DiceResult {
private boolean win;
private BigDecimal payout;
private BigDecimal rolledValue;
public DiceResult(boolean win, BigDecimal payout, BigDecimal rolledValue) {
this.win = win;
this.payout = payout;
this.rolledValue = rolledValue;
}
}

View file

@ -1,69 +0,0 @@
package de.szut.casino.dice;
import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
@Service
public class DiceService {
private static final int MAX_DICE_VALUE = 100;
private final Random random;
private final BalanceService balanceService;
public DiceService(Random random, BalanceService balanceService) {
this.random = random;
this.balanceService = balanceService;
}
public DiceResult play(UserEntity user, DiceDto diceDto) {
balanceService.subtractFunds(user, diceDto.getBetAmount());
int rolledValue = random.nextInt(MAX_DICE_VALUE) + 1;
BigDecimal rolledValueDecimal = BigDecimal.valueOf(rolledValue);
BigDecimal targetValue = diceDto.getTargetValue();
boolean isRollOver = diceDto.isRollOver();
boolean winConditionMet = isWinConditionMet(rolledValueDecimal, targetValue, isRollOver);
if (!winConditionMet) {
return new DiceResult(false, BigDecimal.ZERO, rolledValueDecimal);
}
BigDecimal winChance = calculateWinChance(targetValue, isRollOver);
BigDecimal multiplier = calculateMultiplier(winChance);
BigDecimal payout = diceDto.getBetAmount().multiply(multiplier);
balanceService.addFunds(user, payout);
return new DiceResult(true, payout, rolledValueDecimal);
}
private boolean isWinConditionMet(BigDecimal rolledValue, BigDecimal targetValue, boolean isRollOver) {
if (isRollOver) {
return rolledValue.compareTo(targetValue) > 0;
}
return rolledValue.compareTo(targetValue) < 0;
}
private BigDecimal calculateWinChance(BigDecimal targetValue, boolean isRollOver) {
if (isRollOver) {
return BigDecimal.valueOf(MAX_DICE_VALUE).subtract(targetValue);
}
return targetValue.subtract(BigDecimal.ONE);
}
private BigDecimal calculateMultiplier(BigDecimal winChance) {
if (winChance.compareTo(BigDecimal.ZERO) > 0) {
return BigDecimal.valueOf(MAX_DICE_VALUE - 1).divide(winChance, 4, RoundingMode.HALF_UP);
}
return BigDecimal.ZERO;
}
}

View file

@ -1,10 +1,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;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -21,28 +17,4 @@ public class GlobalExceptionHandler {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<?> handleInsufficientFundsException(InsufficientFundsException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(EntityExistsException.class)
public ResponseEntity<?> handleEntityExistsException(EntityExistsException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.CONFLICT);
}
@ExceptionHandler(EmailNotVerifiedException.class)
public ResponseEntity<?> handleEmailNotVerifiedException(EmailNotVerifiedException ex, WebRequest request) {
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 EmailNotVerifiedException extends Exception {
public EmailNotVerifiedException() {
super("Email not verified");
}
}

View file

@ -1,11 +0,0 @@
package de.szut.casino.exceptionHandling.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException() {
super("insufficient funds");
}
}

View file

@ -1,9 +0,0 @@
package de.szut.casino.exceptionHandling.exceptions;
import org.springframework.security.core.AuthenticationException;
public class OAuth2AuthenticationProcessingException extends AuthenticationException {
public OAuth2AuthenticationProcessingException(String msg) {
super(msg);
}
}

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

@ -6,6 +6,6 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super("User not found");
super("user not found");
}
}

View file

@ -1,85 +1,90 @@
package de.szut.casino.lootboxes;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@RestController
public class LootBoxController {
private final LootBoxRepository lootBoxRepository;
private final UserService userService;
private final LootBoxService lootBoxService;
public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) {
this.lootBoxRepository = lootBoxRepository;
this.userService = userService;
this.lootBoxService = lootBoxService;
}
@GetMapping("/lootboxes")
public List<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll();
}
@PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
UserEntity user = userService.getCurrentUser();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
throw new InsufficientFundsException();
}
RewardEntity reward = lootBoxService.determineReward(lootBox);
lootBoxService.handleBalance(user, lootBox, reward);
return ResponseEntity.ok(reward);
}
@PostMapping("/lootboxes")
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
List<RewardEntity> rewardEntities = new ArrayList<>();
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
}
LootBoxEntity lootBoxEntity = new LootBoxEntity(
createLootBoxDto.getName(),
createLootBoxDto.getPrice(),
rewardEntities
);
this.lootBoxRepository.save(lootBoxEntity);
return ResponseEntity.ok(lootBoxEntity);
}
@DeleteMapping("/lootboxes/{id}")
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
lootBoxRepository.delete(lootBox);
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
}
}
package de.szut.casino.lootboxes;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class LootBoxController {
private final LootBoxRepository lootBoxRepository;
private final UserService userService;
private final LootBoxService lootBoxService;
public LootBoxController(LootBoxRepository lootBoxRepository, UserRepository userRepository, UserService userService, LootBoxService lootBoxService) {
this.lootBoxRepository = lootBoxRepository;
this.userService = userService;
this.lootBoxService = lootBoxService;
}
@GetMapping("/lootboxes")
public List<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll();
}
@PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Insufficient balance");
return ResponseEntity.badRequest().body(errorResponse);
}
RewardEntity reward = lootBoxService.determineReward(lootBox);
lootBoxService.handleBalance(user, lootBox, reward);
return ResponseEntity.ok(reward);
}
@PostMapping("/lootboxes")
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
List<RewardEntity> rewardEntities = new ArrayList<>();
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
}
LootBoxEntity lootBoxEntity = new LootBoxEntity(
createLootBoxDto.getName(),
createLootBoxDto.getPrice(),
rewardEntities
);
this.lootBoxRepository.save(lootBoxEntity);
return ResponseEntity.ok(lootBoxEntity);
}
@DeleteMapping("/lootboxes/{id}")
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
lootBoxRepository.delete(lootBox);
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
}
}

View file

@ -1,40 +1,40 @@
package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class LootBoxService {
private final UserRepository userRepository;
public LootBoxService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
return user.getBalance().compareTo(price) < 0;
}
public RewardEntity determineReward(LootBoxEntity lootBox) {
double randomValue = Math.random();
BigDecimal cumulativeProbability = BigDecimal.ZERO;
for (RewardEntity reward : lootBox.getRewards()) {
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
if (randomValue <= cumulativeProbability.doubleValue()) {
return reward;
}
}
return lootBox.getRewards().getLast();
}
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
user.setBalance(user.getBalance().add(reward.getValue()));
userRepository.save(user);
}
}
package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class LootBoxService {
private final UserRepository userRepository;
public LootBoxService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
return user.getBalance().compareTo(price) < 0;
}
public RewardEntity determineReward(LootBoxEntity lootBox) {
double randomValue = Math.random();
BigDecimal cumulativeProbability = BigDecimal.ZERO;
for (RewardEntity reward : lootBox.getRewards()) {
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
if (randomValue <= cumulativeProbability.doubleValue()) {
return reward;
}
}
return lootBox.getRewards().getLast();
}
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
user.setBalance(user.getBalance().add(reward.getValue()));
userRepository.save(user);
}
}

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

@ -1,60 +0,0 @@
package de.szut.casino.security;
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.dto.LoginRequestDto;
import de.szut.casino.security.dto.ResetPasswordDto;
import de.szut.casino.security.service.AuthService;
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.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException {
AuthResponseDto response = authService.login(loginRequest);
return ResponseEntity.ok(response);
}
@PostMapping("/register")
public ResponseEntity<GetUserDto> registerUser(@Valid @RequestBody CreateUserDto signUpRequest) throws MessagingException, IOException {
GetUserDto response = authService.register(signUpRequest);
return ResponseEntity.ok(response);
}
@PostMapping("/verify")
public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException {
if (authService.verifyEmail(token)) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok().build();
}
@PostMapping("/recover-password")
public ResponseEntity<Void> recoverPassword(@RequestParam("email") String email) throws MessagingException, IOException {
authService.recoverPassword(email);
return ResponseEntity.ok().build();
}
@PostMapping("/reset-password")
public ResponseEntity<Void> resetPassword(@Valid @RequestBody ResetPasswordDto passwordDto) throws MessagingException, IOException {
authService.resetPassword(passwordDto);
return ResponseEntity.ok().build();
}
}

View file

@ -1,40 +0,0 @@
package de.szut.casino.security;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Value("${app.frontend-host}")
private String frontendHost;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", frontendHost);
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Expose-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}

View file

@ -7,7 +7,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
@Override
public AbstractAuthenticationToken convert(Jwt source) {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();

View file

@ -1,21 +1,11 @@
package de.szut.casino.security;
import de.szut.casino.security.jwt.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -25,54 +15,20 @@ import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Value("${app.frontend-host}")
private String frontendHost;
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error", "/oauth2/**").permitAll()
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated();
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/webhook", "/swagger/**", "/swagger-ui/**", "/health").permitAll()
.anyRequest().authenticated();
})
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter())
));
return http.build();
}
@ -80,12 +36,11 @@ public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(this.frontendHost));
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Headers", "x-auth-token"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type", "x-auth-token", "Access-Control-Allow-Origin", "Access-Control-Allow-Methods", "Access-Control-Allow-Headers"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Access-Control-Allow-Origin"));
configuration.setExposedHeaders(List.of("x-auth-token"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;

View file

@ -1,19 +0,0 @@
package de.szut.casino.security.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponseDto {
private String token;
private String tokenType = "Bearer";
public AuthResponseDto(String token) {
this.token = token;
}
}

View file

@ -1,19 +0,0 @@
package de.szut.casino.security.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequestDto {
@NotBlank(message = "Username or email is required")
private String usernameOrEmail;
@NotBlank(message = "Password is required")
private String password;
}

View file

@ -1,65 +0,0 @@
package de.szut.casino.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsService userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null) {
String username = jwtUtils.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}

View file

@ -1,109 +0,0 @@
package de.szut.casino.security.jwt;
import de.szut.casino.security.oauth2.UserPrincipal;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration.ms}")
private int jwtExpirationMs;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes());
}
public String generateToken(Authentication authentication) {
String subject = null;
Map<String, Object> claims = new HashMap<>();
if (authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
subject = userPrincipal.getEmail();
claims.put("id", userPrincipal.getId());
claims.put("username", userPrincipal.getDisplayUsername());
logger.info("Generating token for UserPrincipal: {}", subject);
} else if (authentication.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
subject = (String) oauth2User.getAttributes().get("email");
logger.info("Generating token for OAuth2User: {}", subject);
} else {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
subject = userDetails.getUsername();
logger.info("Generating token for UserDetails: {}", subject);
}
return createToken(claims, subject);
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
logger.info("now: {}", now);
logger.info("jwtExpirationMs: {}", jwtExpirationMs);
logger.info("expiryDate: {}", new Date(now.getTime() + jwtExpirationMs));
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

View file

@ -1,106 +0,0 @@
package de.szut.casino.security.oauth2;
import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException;
import de.szut.casino.user.AuthProvider;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.UUID;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final PasswordEncoder oauth2PasswordEncoder;
public CustomOAuth2UserService(UserRepository userRepository, PasswordEncoder oauth2PasswordEncoder) {
this.userRepository = userRepository;
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
String email = oAuth2UserInfo.getEmail();
if (StringUtils.isEmpty(email)) {
email = oAuth2UserInfo.getName() + "@github.user";
}
Optional<UserEntity> userOptional = userRepository.findByEmail(email);
UserEntity user;
if (userOptional.isPresent()) {
user = userOptional.get();
if (!user.getProvider().equals(AuthProvider.valueOf(registrationId.toUpperCase()))) {
throw new OAuth2AuthenticationProcessingException("You're signed up with " +
user.getProvider() + ". Please use your " + user.getProvider() +
" account to login.");
}
user = updateExistingUser(user, oAuth2UserInfo);
} else {
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo, email);
}
return UserPrincipal.create(user, oAuth2User.getAttributes());
}
private UserEntity registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo, String email) {
UserEntity user = new UserEntity();
String username = oAuth2UserInfo.getName();
if (StringUtils.isEmpty(username)) {
username = "github_" + oAuth2UserInfo.getId();
}
if (userRepository.findByUsername(username).isPresent()) {
username = username + "_" + UUID.randomUUID().toString().substring(0, 8);
}
user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId().toUpperCase()));
user.setProviderId(oAuth2UserInfo.getId());
user.setUsername(username);
user.setEmail(email);
user.setEmailVerified(true);
String randomPassword = UUID.randomUUID().toString();
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
user.setBalance(new BigDecimal("100.00")); // Starting balance
return userRepository.save(user);
}
private UserEntity updateExistingUser(UserEntity existingUser, OAuth2UserInfo oAuth2UserInfo) {
if (!StringUtils.isEmpty(oAuth2UserInfo.getName())) {
existingUser.setUsername(oAuth2UserInfo.getName());
}
return userRepository.save(existingUser);
}
}

View file

@ -1,57 +0,0 @@
package de.szut.casino.security.oauth2;
import de.szut.casino.security.jwt.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationSuccessHandler.class);
@Value("${app.oauth2.authorizedRedirectUris}")
private String redirectUri;
private final JwtUtils jwtUtils;
public OAuth2AuthenticationSuccessHandler(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String targetUrl = determineTargetUrl(authentication);
logger.info("OAuth2 Authentication successful, redirecting to: {}", targetUrl);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
clearAuthenticationAttributes(request);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
private String determineTargetUrl(Authentication authentication) {
String token = jwtUtils.generateToken(authentication);
if (authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
logger.info("User authenticated: ID={}, Email={}", userPrincipal.getId(), userPrincipal.getEmail());
}
return UriComponentsBuilder.fromUriString(redirectUri)
.queryParam("token", token)
.build().toUriString();
}
}

View file

@ -1,15 +0,0 @@
package de.szut.casino.security.oauth2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class OAuth2Config {
@Bean
public PasswordEncoder oauth2PasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View file

@ -1,20 +0,0 @@
package de.szut.casino.security.oauth2;
import lombok.Getter;
import java.util.Map;
@Getter
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
}

View file

@ -1,21 +0,0 @@
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;
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) {
return new GitHubOAuth2UserInfo(attributes);
} else if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.toString())) {
return new GoogleOAuth2UserInfo(attributes);
} else {
throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
}
}
}

View file

@ -1,102 +0,0 @@
package de.szut.casino.security.oauth2;
import de.szut.casino.user.UserEntity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class UserPrincipal implements OAuth2User, UserDetails {
@Getter
private Long id;
@Getter
private String email;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Setter
private Map<String, Object> attributes;
public UserPrincipal(Long id, String email, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.email = email;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(UserEntity user) {
List<GrantedAuthority> authorities = Collections.
singletonList(new SimpleGrantedAuthority("ROLE_USER"));
return new UserPrincipal(
user.getId(),
user.getEmail(),
user.getUsername(),
user.getPassword(),
authorities
);
}
public static UserPrincipal create(UserEntity user, Map<String, Object> attributes) {
UserPrincipal userPrincipal = UserPrincipal.create(user);
userPrincipal.setAttributes(attributes);
return userPrincipal;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
public String getDisplayUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return String.valueOf(id);
}
}

View file

@ -1,50 +0,0 @@
package de.szut.casino.security.oauth2.github;
import de.szut.casino.security.dto.AuthResponseDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
@RestController
@RequestMapping("/oauth2/github")
public class GitHubController {
private static final Logger logger = LoggerFactory.getLogger(GitHubController.class);
@Value("${spring.security.oauth2.client.registration.github.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.provider.github.authorization-uri}")
private String authorizationUri;
@Value("${spring.security.oauth2.client.registration.github.redirect-uri}")
private String redirectUri;
private final GitHubService githubService;
public GitHubController(GitHubService githubService) {
this.githubService = githubService;
}
@GetMapping("/authorize")
public RedirectView authorizeGithub() {
logger.info("Redirecting to GitHub for authorization");
String authUrl = authorizationUri +
"?client_id=" + clientId +
"&redirect_uri=" + redirectUri +
"&scope=user:email,read:user";
return new RedirectView(authUrl);
}
@PostMapping("/callback")
public ResponseEntity<AuthResponseDto> githubCallback(@RequestBody GithubCallbackDto githubCallbackDto) {
String code = githubCallbackDto.getCode();
AuthResponseDto response = githubService.processGithubCode(code);
return ResponseEntity.ok(response);
}
}

View file

@ -1,27 +0,0 @@
package de.szut.casino.security.oauth2.github;
import de.szut.casino.security.oauth2.OAuth2UserInfo;
import java.util.Map;
public class GitHubOAuth2UserInfo extends OAuth2UserInfo {
public GitHubOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return ((Integer) attributes.get("id")).toString();
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
}

View file

@ -1,172 +0,0 @@
package de.szut.casino.security.oauth2.github;
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.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.*;
@Service
public class GitHubService {
@Value("${spring.security.oauth2.client.registration.github.client-id}")
private String clientId;
@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;
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;
}
public AuthResponseDto processGithubCode(String code) {
try {
RestTemplate restTemplate = new RestTemplate();
Map<String, String> requestBody = new HashMap<>();
requestBody.put("client_id", clientId);
requestBody.put("client_secret", clientSecret);
requestBody.put("code", code);
HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
"https://github.com/login/oauth/access_token",
HttpMethod.POST,
requestEntity,
Map.class
);
Map<String, Object> responseBody = response.getBody();
if (responseBody.containsKey("error")) {
String error = (String) responseBody.get("error");
String errorDescription = (String) responseBody.get("error_description");
throw new RuntimeException("GitHub OAuth error: " + errorDescription);
}
String accessToken = (String) responseBody.get("access_token");
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Failed to receive access token from GitHub");
}
HttpHeaders userInfoHeaders = new HttpHeaders();
userInfoHeaders.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders);
ResponseEntity<Map> userResponse = restTemplate.exchange(
"https://api.github.com/user",
HttpMethod.GET,
userInfoRequestEntity,
Map.class
);
Map<String, Object> userAttributes = userResponse.getBody();
HttpHeaders emailsHeaders = new HttpHeaders();
emailsHeaders.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> emailsRequestEntity = new HttpEntity<>(null, emailsHeaders);
ResponseEntity<List> emailsResponse = restTemplate.exchange(
"https://api.github.com/user/emails",
HttpMethod.GET,
emailsRequestEntity,
List.class
);
List<Map<String, Object>> emails = emailsResponse.getBody();
String email = null;
for (Map<String, Object> emailInfo : emails) {
Boolean primary = (Boolean) emailInfo.get("primary");
if (primary != null && primary) {
email = (String) emailInfo.get("email");
break;
}
}
if (email == null && !emails.isEmpty()) {
email = (String) emails.get(0).get("email");
}
String githubId = userAttributes.get("id").toString();
String username = (String) userAttributes.get("login");
Optional<UserEntity> userOptional = userRepository.findByProviderId(githubId);
UserEntity user;
if (userOptional.isPresent()) {
user = userOptional.get();
} else {
userOptional = userRepository.findByEmail(email);
if (userOptional.isPresent()) {
user = userOptional.get();
user.setProvider(AuthProvider.GITHUB);
user.setProviderId(githubId);
} else {
user = new UserEntity();
user.setEmail(email);
user.setUsername(username);
user.setProvider(AuthProvider.GITHUB);
user.setProviderId(githubId);
user.setEmailVerified(true);
user.setBalance(new BigDecimal("100.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));
String token = jwtUtils.generateToken(authentication);
return new AuthResponseDto(token);
} catch (Exception e) {
throw new RuntimeException("Failed to process GitHub authentication", e);
}
}
}

View file

@ -1,8 +0,0 @@
package de.szut.casino.security.oauth2.github;
import lombok.Data;
@Data
public class GithubCallbackDto {
private String code;
}

View file

@ -1,51 +0,0 @@
package de.szut.casino.security.oauth2.google;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.oauth2.github.GithubCallbackDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
@RestController
@RequestMapping("/oauth2/google")
public class GoogleController {
private static final Logger logger = LoggerFactory.getLogger(GoogleController.class);
@Value("${spring.security.oauth2.client.registration.google.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.provider.google.authorization-uri}")
private String authorizationUri;
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
private String redirectUri;
private final GoogleService googleService;
public GoogleController(GoogleService googleService) {
this.googleService = googleService;
}
@GetMapping("/authorize")
public RedirectView authorizeGoogle() {
logger.info("Redirecting to Google for authorization");
String authUrl = authorizationUri +
"?client_id=" + clientId +
"&redirect_uri=" + redirectUri +
"&response_type=code" +
"&scope=email profile";
return new RedirectView(authUrl);
}
@PostMapping("/callback")
public ResponseEntity<AuthResponseDto> googleCallback(@RequestBody GithubCallbackDto callbackDto) {
String code = callbackDto.getCode();
AuthResponseDto response = googleService.processGoogleCode(code);
return ResponseEntity.ok(response);
}
}

View file

@ -1,27 +0,0 @@
package de.szut.casino.security.oauth2.google;
import de.szut.casino.security.oauth2.OAuth2UserInfo;
import java.util.Map;
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return (String) attributes.get("sub");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
}

View file

@ -1,176 +0,0 @@
package de.szut.casino.security.oauth2.google;
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.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
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;
@Service
public class GoogleService {
private static final Logger logger = LoggerFactory.getLogger(GoogleService.class);
@Value("${spring.security.oauth2.client.registration.google.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
private String clientSecret;
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
private String redirectUri;
@Value("${spring.security.oauth2.client.provider.google.token-uri}")
private String tokenUri;
@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;
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;
}
public AuthResponseDto processGoogleCode(String code) {
try {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders tokenHeaders = new HttpHeaders();
tokenHeaders.set("Content-Type", "application/x-www-form-urlencoded");
MultiValueMap<String, String> tokenRequestBody = new LinkedMultiValueMap<>();
tokenRequestBody.add("client_id", clientId);
tokenRequestBody.add("client_secret", clientSecret);
tokenRequestBody.add("code", code);
tokenRequestBody.add("redirect_uri", redirectUri);
tokenRequestBody.add("grant_type", "authorization_code");
HttpEntity<MultiValueMap<String, String>> tokenRequestEntity = new HttpEntity<>(tokenRequestBody, tokenHeaders);
ResponseEntity<Map> tokenResponse = restTemplate.exchange(
tokenUri,
HttpMethod.POST,
tokenRequestEntity,
Map.class
);
Map<String, Object> tokenResponseBody = tokenResponse.getBody();
if (tokenResponseBody == null || tokenResponseBody.containsKey("error")) {
String error = tokenResponseBody != null ? (String) tokenResponseBody.get("error") : "Unknown error";
throw new RuntimeException("Google OAuth error: " + error);
}
String accessToken = (String) tokenResponseBody.get("access_token");
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Failed to receive access token from Google");
}
HttpHeaders userInfoHeaders = new HttpHeaders();
userInfoHeaders.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders);
ResponseEntity<Map> userResponse = restTemplate.exchange(
userInfoUri,
HttpMethod.GET,
userInfoRequestEntity,
Map.class
);
Map<String, Object> userAttributes = userResponse.getBody();
if (userAttributes == null) {
throw new RuntimeException("Failed to fetch user data from Google");
}
String googleId = (String) userAttributes.get("sub");
String email = (String) userAttributes.get("email");
String name = (String) userAttributes.get("name");
Boolean emailVerified = (Boolean) userAttributes.getOrDefault("email_verified", false);
if (email == null) {
throw new RuntimeException("Google account does not have an email");
}
String username = name != null ? name.replaceAll("\\s+", "") : email.split("@")[0];
Optional<UserEntity> userOptional = userRepository.findByProviderId(googleId);
UserEntity user;
if (userOptional.isPresent()) {
user = userOptional.get();
} else {
userOptional = userRepository.findByEmail(email);
if (userOptional.isPresent()) {
user = userOptional.get();
user.setProvider(AuthProvider.GOOGLE);
user.setProviderId(googleId);
} else {
user = new UserEntity();
user.setEmail(email);
user.setUsername(username);
user.setProvider(AuthProvider.GOOGLE);
user.setProviderId(googleId);
user.setEmailVerified(emailVerified);
user.setBalance(new BigDecimal("100.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 = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)
);
String token = jwtUtils.generateToken(authentication);
return new AuthResponseDto(token);
} catch (Exception e) {
logger.error("Failed to process Google authentication", e);
throw new RuntimeException("Failed to process Google authentication", e);
}
}
}

View file

@ -1,108 +0,0 @@
package de.szut.casino.security.service;
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.dto.LoginRequestDto;
import de.szut.casino.security.dto.ResetPasswordDto;
import de.szut.casino.security.jwt.JwtUtils;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
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.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.io.IOException;
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;
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;
}
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
throw new EmailNotVerifiedException();
}
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateToken(authentication);
return new AuthResponseDto(jwt);
}
public GetUserDto register(CreateUserDto signUpRequest) throws MessagingException, IOException {
UserEntity user = userService.createUser(signUpRequest);
this.emailService.sendEmailVerificationEmail(user);
return new GetUserDto(
user.getId(),
user.getEmail(),
user.getUsername(),
user.getBalance()
);
}
public Boolean verifyEmail(String token) throws MessagingException, IOException {
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
if (!optionalUser.isPresent()) {
return false;
}
UserEntity user = optionalUser.get();
user.setEmailVerified(true);
user.setVerificationToken(null);
userService.saveUser(user);
this.emailService.sendWelcomeEmail(user);
return true;
}
public void recoverPassword(String email) throws MessagingException, IOException {
Optional<UserEntity> optionalUser = userService.getUserByEmail(email);
if (optionalUser.isPresent()) {
UserEntity user = optionalUser.get();
user.setPasswordResetToken(RandomStringUtils.randomAlphanumeric(64));
userService.saveUser(user);
this.emailService.sendPasswordRecoveryEmail(user);
}
}
public void resetPassword(ResetPasswordDto passwordDto) {
Optional<UserEntity> optionalUser = userService.getUserByPasswordResetToken(passwordDto.getToken());
if (optionalUser.isPresent()) {
UserEntity user = optionalUser.get();
user.setPassword(passwordEncoder.encode(passwordDto.getPassword()));
user.setPasswordResetToken(null);
userService.saveUser(user);
}
}
}

View file

@ -1,115 +0,0 @@
package de.szut.casino.security.service;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.user.UserEntity;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
@Service
public class EmailService {
private JavaMailSenderImpl mailSender;
private MailConfig mailConfig;
@Value("${app.frontend-host}")
private String feUrl;
public EmailService(JavaMailSenderImpl mailSender, MailConfig mailConfig) {
this.mailSender = mailSender;
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);
}
}
public void sendEmailVerificationEmail(UserEntity user) throws IOException, MessagingException {
String template = loadTemplate("email/verify.html");
String htmlContent = template
.replace("${username}", user.getUsername())
.replace("${feUrl}", feUrl)
.replace("${token}", user.getVerificationToken());
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(mailConfig.fromAddress);
helper.setTo(user.getEmailAddress());
helper.setSubject("E-Mail Bestätigung");
helper.setText(htmlContent, true);
mailSender.send(message);
}
public void sendWelcomeEmail(UserEntity user) throws IOException, MessagingException {
String template = loadTemplate("email/welcome.html");
String htmlContent = template
.replace("${username}", user.getUsername())
.replace("${feUrl}", feUrl);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(mailConfig.fromAddress);
helper.setTo(user.getEmailAddress());
helper.setSubject("Willkommen bei Trustworthy Casino©");
helper.setText(htmlContent, true);
mailSender.send(message);
}
public void sendDepositEmail(TransactionEntity transaction) throws IOException, MessagingException {
String template = loadTemplate("email/deposit.html");
String htmlContent = template
.replace("${username}", transaction.getUser().getUsername())
.replace("${amount}", String.valueOf(transaction.getAmount()))
.replace("${feUrl}", feUrl);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(mailConfig.fromAddress);
helper.setTo(transaction.getUser().getEmailAddress());
helper.setSubject("Einzahlung über ${amount}€ Erfolgreich".replace("${amount}", String.valueOf(transaction.getAmount())));
helper.setText(htmlContent, true);
mailSender.send(message);
}
public void sendPasswordRecoveryEmail(UserEntity user) throws IOException, MessagingException {
String template = loadTemplate("email/recover-password.html");
String htmlContent = template
.replace("${username}", user.getUsername())
.replace("${resetToken}", user.getPasswordResetToken())
.replace("${feUrl}", feUrl);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(mailConfig.fromAddress);
helper.setTo(user.getEmailAddress());
helper.setSubject("Zurücksetzen ihres Passworts");
helper.setText(htmlContent, true);
mailSender.send(message);
}
private String loadTemplate(String templatePath) throws IOException {
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
}
}
}

View file

@ -1,28 +0,0 @@
package de.szut.casino.security.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class MailConfig {
@Value("${app.mail.host}")
public String host;
@Value("${app.mail.port}")
public Integer port;
@Value("${app.mail.authentication}")
public Boolean authenticationEnabled;
@Value("${app.mail.username}")
public String username;
@Value("${app.mail.password}")
public String password;
@Value("${app.mail.from-address}")
public String fromAddress;
@Value("${app.mail.protocol}")
public String protocol;
}

View file

@ -1,38 +0,0 @@
package de.szut.casino.security.service;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Optional;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail);
if (user.isEmpty()) {
user = userRepository.findByEmail(usernameOrEmail);
}
UserEntity userEntity = user.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail));
return new org.springframework.security.core.userdetails.User(
userEntity.getUsername(),
userEntity.getPassword(),
new ArrayList<>());
}
}

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

@ -9,7 +9,7 @@ import java.math.BigDecimal;
@Service
public class BalanceService {
private final UserRepository userRepository;
private UserRepository userRepository;
public BalanceService(UserRepository userRepository) {
this.userRepository = userRepository;

View file

@ -1,6 +1,5 @@
package de.szut.casino.slots;
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;
@ -8,14 +7,12 @@ import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;
import java.util.Optional;
@RestController
@ -31,11 +28,17 @@ public class SlotController {
}
@PostMapping("/slots/spin")
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto) {
UserEntity user = userService.getCurrentUser();
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
UserEntity user = optionalUser.get();
if (!this.balanceService.hasFunds(user, betDto)) {
throw new InsufficientFundsException();
return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds"));
}
SpinResult spinResult = this.slotService.spin(
@ -45,15 +48,4 @@ public class SlotController {
return ResponseEntity.ok(spinResult);
}
@GetMapping("/slots/info")
public ResponseEntity<Object> spinSlots() {
Map<String, BigDecimal> info = new HashMap<>();
for (Symbol symbol : Symbol.values()) {
info.put(symbol.getDisplayName(), symbol.getPayoutMultiplier());
}
return ResponseEntity.ok(info);
}
}

View file

@ -10,12 +10,23 @@ import java.util.Collections;
import java.util.List;
import java.util.Random;
import static de.szut.casino.slots.Symbol.*;
@Service
public class SlotService {
private final int REEL_LENGTH = 32;
// 98% RTP
private final int SEVEN_COUNT = 1;
private final int BAR_COUNT = 4;
private final int BELL_COUNT = 7;
private final int CHERRY_COUNT = 10;
private final int BLANK_COUNT = 10;
private final Symbol SEVEN = new Symbol("seven", new BigDecimal("1000"));
private final Symbol BAR = new Symbol("bar", new BigDecimal("85"));
private final Symbol BELL = new Symbol("bell", new BigDecimal("40"));
private final Symbol CHERRY = new Symbol("cherry", new BigDecimal("10"));
private final Symbol BLANK = new Symbol("blank", new BigDecimal("0"));
private final List<Symbol> firstReel;
private final List<Symbol> secondReel;
private final List<Symbol> thirdReel;
@ -42,28 +53,33 @@ public class SlotService {
Symbol symbol2 = getSymbolAt(this.secondReel, index2);
Symbol symbol3 = getSymbolAt(this.thirdReel, index3);
Status status = determineStatus(symbol1, symbol2, symbol3);
boolean isWin = symbol1.equals(symbol2) && symbol1.equals(symbol3);
SpinResult spinResult = processResult(betAmount, user, status, symbol1);
SpinResult spinResult = processResult(betAmount, user, isWin, symbol1);
buildResultMatrix(spinResult, index1, index2, index3);
return spinResult;
}
private SpinResult processResult(BigDecimal betAmount, UserEntity user, Status status, Symbol winSymbol) {
SpinResult spinResult = new SpinResult();
spinResult.setStatus(status.name().toLowerCase());
private SpinResult processResult(BigDecimal betAmount, UserEntity user, boolean isWin, Symbol winSymbol) {
BigDecimal resultAmount;
String status;
this.balanceService.subtractFunds(user, betAmount);
if (status == Status.WIN) {
BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier());
this.balanceService.addFunds(user, winAmount);
spinResult.setAmount(winAmount);
if (isWin) {
resultAmount = betAmount.multiply(winSymbol.getPayoutMultiplier());
status = "win";
this.balanceService.addFunds(user, resultAmount);
} else {
spinResult.setAmount(betAmount);
resultAmount = betAmount;
status = "lose";
this.balanceService.subtractFunds(user, betAmount);
}
SpinResult spinResult = new SpinResult();
spinResult.setStatus(status);
spinResult.setAmount(resultAmount);
spinResult.setWin(isWin);
return spinResult;
}
@ -97,11 +113,11 @@ public class SlotService {
private List<Symbol> createReelStrip() {
List<Symbol> reelStrip = new ArrayList<>(REEL_LENGTH);
addSymbolsToStrip(reelStrip, CHERRY, CHERRY.getCountPerStrip());
addSymbolsToStrip(reelStrip, BELL, BELL.getCountPerStrip());
addSymbolsToStrip(reelStrip, BAR, BAR.getCountPerStrip());
addSymbolsToStrip(reelStrip, SEVEN, SEVEN.getCountPerStrip());
addSymbolsToStrip(reelStrip, BLANK, BLANK.getCountPerStrip());
addSymbolsToStrip(reelStrip, CHERRY, CHERRY_COUNT);
addSymbolsToStrip(reelStrip, BELL, BELL_COUNT);
addSymbolsToStrip(reelStrip, BAR, BAR_COUNT);
addSymbolsToStrip(reelStrip, SEVEN, SEVEN_COUNT);
addSymbolsToStrip(reelStrip, BLANK, BLANK_COUNT);
return reelStrip;
}
@ -120,18 +136,4 @@ public class SlotService {
return reel.get(effectiveIndex);
}
private Status determineStatus(Symbol symbol1, Symbol symbol2, Symbol symbol3) {
boolean allSymbolsMatch = symbol1.equals(symbol2) && symbol1.equals(symbol3);
if (allSymbolsMatch) {
if (symbol1 == Symbol.BLANK) {
return Status.BLANK;
} else {
return Status.WIN;
}
}
return Status.LOSE;
}
}

View file

@ -11,7 +11,14 @@ import java.util.List;
@Setter
@NoArgsConstructor
public class SpinResult {
public SpinResult(String status, BigDecimal amount, boolean isWin) {
this.status = status;
this.amount = amount;
this.isWin = isWin;
}
private String status;
private BigDecimal amount;
private boolean isWin;
private List<List<Symbol>> resultMatrix;
}

View file

@ -1,7 +0,0 @@
package de.szut.casino.slots;
public enum Status {
WIN,
LOSE,
BLANK
}

View file

@ -1,24 +1,35 @@
package de.szut.casino.slots;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
public enum Symbol {
SEVEN("seven", new BigDecimal("1000"), 1),
BAR("bar", new BigDecimal("85"), 4),
BELL("bell", new BigDecimal("40"), 7),
CHERRY("cherry", new BigDecimal("10"), 10),
BLANK("blank", new BigDecimal("0"), 10);
@Setter
@AllArgsConstructor
public class Symbol {
private String name;
private BigDecimal payoutMultiplier;
private final String displayName;
private final BigDecimal payoutMultiplier;
private final int countPerStrip;
@Override
public boolean equals(Object other) {
if (!(other instanceof Symbol that)) {
return false;
}
Symbol(String displayName, BigDecimal payoutMultiplier, int count) {
this.displayName = displayName;
this.payoutMultiplier = payoutMultiplier;
this.countPerStrip = count;
return this.name.equals(that.name) && this.payoutMultiplier.equals(that.payoutMultiplier);
}
@Override
public int hashCode() {
int hashCode = 1;
hashCode = hashCode * 37 + this.name.hashCode();
hashCode = hashCode * 37 + this.payoutMultiplier.hashCode();
return hashCode;
}
}

View file

@ -1,7 +0,0 @@
package de.szut.casino.user;
public enum AuthProvider {
LOCAL,
GITHUB,
GOOGLE
}

View file

@ -1,30 +1,43 @@
package de.szut.casino.user;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
private UserService userService;
private final UserMappingService userMappingService;
@PostMapping("/user")
public ResponseEntity<?> createUser(@RequestBody @Valid CreateUserDto userData) {
if (userService.exists(userData.getAuthentikId())) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", "/user");
public UserController(UserService userService, UserMappingService userMappingService) {
this.userService = userService;
this.userMappingService = userMappingService;
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
return ResponseEntity.ok(userService.createUser(userData));
}
@GetMapping("/me")
public ResponseEntity<GetUserDto> getCurrentUser() {
return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser()));
@GetMapping("/user")
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
GetUserDto userData = userService.getCurrentUserAsDto(token);
if (userData == null) {
throw new UserNotFoundException();
}
return ResponseEntity.ok(userData);
}
}

View file

@ -1,6 +1,9 @@
package de.szut.casino.user;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@ -15,47 +18,17 @@ public class UserEntity {
@Id
@GeneratedValue
private Long id;
@Version
private Long version;
@Column(unique = true)
private String email;
@Column(unique = true)
private String authentikId;
private String username;
private String password;
@Column(precision = 19, scale = 2)
private BigDecimal balance;
private Boolean emailVerified = false;
private String verificationToken;
private String passwordResetToken;
@Enumerated(EnumType.STRING)
private AuthProvider provider = AuthProvider.LOCAL;
private String providerId;
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
this.email = email;
public UserEntity(String authentikId, String username, BigDecimal balance) {
this.authentikId = authentikId;
this.username = username;
this.password = password;
this.balance = balance;
this.verificationToken = verificationToken;
}
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {
this.email = email;
this.username = username;
this.provider = provider;
this.providerId = providerId;
this.balance = balance;
this.emailVerified = true; // OAuth providers verify emails
}
public void addBalance(BigDecimal amountToAdd) {
@ -85,8 +58,4 @@ public class UserEntity {
this.balance = this.balance.subtract(amountToSubtract);
}
public String getEmailAddress() {
return "${name} <${email}>".replace("${name}", this.username).replace("${email}", this.email);
}
}

View file

@ -1,13 +1,19 @@
package de.szut.casino.user;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class UserMappingService {
public GetUserDto mapToGetUserDto(UserEntity user) {
return new GetUserDto(user.getId(), user.getEmail(), user.getUsername(), user.getBalance());
return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance());
}
public UserEntity mapToUserEntity(CreateUserDto createUserDto) {
return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO);
}
}

View file

@ -8,22 +8,8 @@ import java.util.Optional;
@Service
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
@Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1")
Optional<UserEntity> findOneByAuthentikId(String authentikId);
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByProviderId(String providerId);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
@Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1")
Optional<UserEntity> findOneByVerificationToken(String token);
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
@Query("SELECT u FROM UserEntity u WHERE u.passwordResetToken = ?1")
Optional<UserEntity> findOneByPasswordResetToken(String token);
boolean existsByAuthentikId(String authentikId);
}

View file

@ -1,88 +1,81 @@
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.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import de.szut.casino.user.dto.GetUserDto;
import de.szut.casino.user.dto.KeycloakUserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
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 RestTemplate http;
@Autowired
private UserMappingService mappingService;
public UserEntity createUser(CreateUserDto createUserDto) {
if (userRepository.existsByUsername(createUserDto.getUsername())) {
throw new EntityExistsException("Username is already taken");
}
if (userRepository.existsByEmail(createUserDto.getEmail())) {
throw new EntityExistsException("Email is already in use");
}
UserEntity user = new UserEntity(
createUserDto.getEmail(),
createUserDto.getUsername(),
passwordEncoder.encode(createUserDto.getPassword()),
BigDecimal.valueOf(100),
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() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
if (optionalUser.isEmpty()) {
throw new UserNotFoundException();
}
return optionalUser.get();
}
public Optional<UserEntity> getUserByVerificationToken(String token) {
return this.userRepository.findOneByVerificationToken(token);
}
public void saveUser(UserEntity user) {
UserEntity user = mappingService.mapToUserEntity(createUserDto);
userRepository.save(user);
return user;
}
public boolean isVerified(String usernameOrEmail) {
Optional<UserEntity> optionalUser = userRepository.findOneByUsernameOrEmail(usernameOrEmail);
public GetUserDto getUser(String authentikId) {
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(authentikId);
if (!optionalUser.isPresent()) {
return false;
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
}
public GetUserDto getCurrentUserAsDto(String token) {
KeycloakUserDto userData = getAuthentikUserInfo(token);
if (userData == null) {
return null;
}
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(userData.getSub());
return optionalUser.get().getEmailVerified();
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
}
public Optional<UserEntity> getUserByEmail(String email) {
return userRepository.findByEmail(email);
public Optional<UserEntity> getCurrentUser(String token) {
KeycloakUserDto userData = getAuthentikUserInfo(token);
if (userData == null) {
return Optional.empty();
}
return this.userRepository.findOneByAuthentikId(userData.getSub());
}
public Optional<UserEntity> getUserByPasswordResetToken(String token) {
return this.userRepository.findOneByPasswordResetToken(token);
private KeycloakUserDto getAuthentikUserInfo(String token) {
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
ResponseEntity<KeycloakUserDto> response = this.http.exchange(
"https://oauth.simonis.lol/application/o/userinfo/",
HttpMethod.GET,
new HttpEntity<>(headers),
KeycloakUserDto.class
);
return response.getBody();
} catch (Exception e) {
System.err.println("Error fetching user info from Authentik: " + e.getMessage());
return null;
}
}
public boolean exists(String authentikId) {
return userRepository.existsByAuthentikId(authentikId);
}
}

View file

@ -1,8 +1,5 @@
package de.szut.casino.user.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@ -13,15 +10,6 @@ import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
public class CreateUserDto {
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String authentikId;
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
}

View file

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

View file

@ -1,15 +1,15 @@
package de.szut.casino.security.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class ResetPasswordDto {
private String token;
private String password;
}
package de.szut.casino.user.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class KeycloakUserDto {
private String sub;
private String preferred_username;
}

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(authToken);
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

@ -5,50 +5,36 @@ server.port=${HTTP_PORT:8080}
spring.jpa.hibernate.ddl-auto=update
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
app.frontend-host=${FE_URL:http://localhost:4200}
app.mail.authentication=${MAIL_AUTHENTICATION:false}
app.mail.host=${MAIL_HOST:localhost}
app.mail.port=${MAIL_PORT:1025}
app.mail.username=${MAIL_USER:null}
app.mail.password=${MAIL_PASS:null}
app.mail.from-address=${MAIL_FROM:casino@localhost}
app.mail.protocol=${MAIL_PROTOCOL:smtp}
spring.application.name=casino
#client registration configuration
# JWT Configuration
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
spring.security.oauth2.client.registration.authentik.client-id=${AUTH_CLIENT_ID:MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm}
spring.security.oauth2.client.registration.authentik.client-secret=${AUTH_CLIENT_SECRET:GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5}
spring.security.oauth2.client.registration.authentik.provider=authentik
spring.security.oauth2.client.registration.authentik.client-name=Authentik
spring.security.oauth2.client.registration.authentik.scope=openid,email,profile
spring.security.oauth2.client.registration.authentik.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
# Logging
# Provider settings
spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTH_PROVIDER_ISSUER:https://oauth.simonis.lol/application/o/casino-dev/}
spring.security.oauth2.client.provider.authentik.authorization-uri=${AUTH_PROVIDER_AUTHORIZE_URI:https://oauth.simonis.lol/application/o/authorize/}
spring.security.oauth2.client.provider.authentik.token-uri=${AUTH_PROVIDER_TOKEN_URI:https://oauth.simonis.lol/application/o/token/}
spring.security.oauth2.client.provider.authentik.user-info-uri=${AUTH_PROVIDER_USERINFO_URI:https://oauth.simonis.lol/application/o/userinfo/}
spring.security.oauth2.client.provider.authentik.jwk-set-uri=${AUTH_PROVIDER_JWKS_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/}
spring.security.oauth2.client.provider.authentik.user-name-attribute=${AUTH_PROVIDER_NAME_ATTR:preferred_username}
# Resource server config
spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTH_JWT_ISSUER_URI:https://oauth.simonis.lol/application/o/casino-dev}/
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${AUTH_JWT_JWT_SET_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/}
#OIDC provider configuration:
logging.level.org.springframework.security=DEBUG
#validating JWT token against our Authentik server
# 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

@ -1,136 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einzahlung bestätigt - Trustworthy Casino©</title>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
background-color: #f8fafc;
color: #64748b;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #0a1219;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
background-color: #1a2835;
padding: 20px;
text-align: center;
}
.header h1 {
color: #ffffff;
margin: 0;
font-size: 28px;
}
.content {
background-color: #121e27;
padding: 30px;
color: #ffffff;
}
.footer {
background-color: #1a2835;
color: #94a3b8;
padding: 20px;
text-align: center;
font-size: 0.8em;
}
.button {
display: inline-block;
background-color: #10b981;
color: #ffffff;
padding: 12px 24px;
margin: 20px 0;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
transition: background-color 0.3s;
}
.button:hover {
background-color: #059669;
}
h2 {
color: #ffffff;
padding-bottom: 10px;
display: inline-block;
}
ul {
padding-left: 20px;
margin: 20px 0;
}
li {
margin-bottom: 8px;
color: #94a3b8;
}
li::marker {
color: #34d399;
}
.highlight {
color: #10b981;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #1a2835;
margin: 20px 0;
}
p {
margin: 16px 0;
}
.amount-box {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
text-align: center;
margin: 20px 0;
}
.amount-value {
font-size: 28px;
font-weight: bold;
color: #10b981;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Trustworthy Casino</h1>
</div>
<div class="content">
<h2>Hallo <span class="highlight">${username}</span>,</h2>
<p>vielen Dank für Ihre Einzahlung bei Trustworthy Casino. Wir bestätigen den Eingang Ihres Guthabens.</p>
<div class="amount-box">
<p>Eingezahlter Betrag</p>
<div class="amount-value">${amount}€</div>
</div>
<div class="divider"></div>
<p>Ihr Guthaben wurde Ihrem Konto sofort gutgeschrieben und steht ab sofort zum Spielen zur Verfügung.</p>
<div style="text-align: center;">
<a href="${feUrl}/games" class="button">Jetzt Spielen</a>
</div>
<p>Bei Fragen zu Ihrer Einzahlung kontaktieren Sie bitte unseren Kundenservice.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
</div>
<div class="footer">
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
</div>
</div>
</body>
</html>

View file

@ -1,156 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - Trustworthy Casino©</title>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
background-color: #f8fafc;
color: #64748b;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #0a1219;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
background-color: #1a2835;
padding: 20px;
text-align: center;
}
.header h1 {
color: #ffffff;
margin: 0;
font-size: 28px;
}
.content {
background-color: #121e27;
padding: 30px;
color: #ffffff;
}
.footer {
background-color: #1a2835;
color: #94a3b8;
padding: 20px;
text-align: center;
font-size: 0.8em;
}
.button {
display: inline-block;
background-color: #10b981;
color: #ffffff;
padding: 12px 24px;
margin: 20px 0;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
transition: background-color 0.3s;
}
.button:hover {
background-color: #059669;
}
h2 {
color: #ffffff;
padding-bottom: 10px;
display: inline-block;
}
ul {
padding-left: 20px;
margin: 20px 0;
}
li {
margin-bottom: 8px;
color: #94a3b8;
}
li::marker {
color: #34d399;
}
.highlight {
color: #10b981;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #1a2835;
margin: 20px 0;
}
p {
margin: 16px 0;
}
.recovery-code {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
text-align: center;
margin: 20px 0;
letter-spacing: 5px;
font-size: 24px;
font-weight: bold;
color: #10b981;
}
.info-box {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
}
.warning {
color: #f59e0b;
font-size: 0.9em;
}
.security-info {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Trustworthy Casino</h1>
</div>
<div class="content">
<h2>Hallo <span class="highlight">${username}</span>,</h2>
<p>wir haben eine Anfrage zum Zurücksetzen Ihres Passworts für Ihr Trustworthy Casino Konto erhalten. Um Ihr Passwort zurückzusetzen, klicken Sie bitte auf den folgenden Button:</p>
<div style="text-align: center;">
<a href="${feUrl}/reset-password?token=${resetToken}" class="button">Passwort zurücksetzen</a>
</div>
<div class="info-box">
<p><span class="warning">Hinweis:</span> Dieser Link und Code sind aus Sicherheitsgründen vielleicht nur <span class="highlight">60 Minuten</span> gültig.</p>
</div>
<div class="divider"></div>
<div class="security-info">
<p>Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail bitte. In diesem Fall empfehlen wir Ihnen, Ihr Passwort zu ändern und unseren Kundenservice zu kontaktieren, um die Sicherheit Ihres Kontos zu gewährleisten.</p>
</div>
<div class="divider"></div>
<p>Bei Fragen steht Ihnen unser Support-Team nicht zur Verfügung.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
</div>
<div class="footer">
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
<p>Für Fragen zur Sicherheit Ihres Kontos kontaktieren Sie uns bitte nicht.</p>
</div>
</div>
</body>
</html>

View file

@ -1,149 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Mail-Verifizierung - Trustworthy Casino©</title>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
background-color: #f8fafc;
color: #64748b;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #0a1219;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
background-color: #1a2835;
padding: 20px;
text-align: center;
}
.header h1 {
color: #ffffff;
margin: 0;
font-size: 28px;
}
.content {
background-color: #121e27;
padding: 30px;
color: #ffffff;
}
.footer {
background-color: #1a2835;
color: #94a3b8;
padding: 20px;
text-align: center;
font-size: 0.8em;
}
.button {
display: inline-block;
background-color: #10b981;
color: #ffffff;
padding: 12px 24px;
margin: 20px 0;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
transition: background-color 0.3s;
}
.button:hover {
background-color: #059669;
}
h2 {
color: #ffffff;
padding-bottom: 10px;
display: inline-block;
}
ul {
padding-left: 20px;
margin: 20px 0;
}
li {
margin-bottom: 8px;
color: #94a3b8;
}
li::marker {
color: #34d399;
}
.highlight {
color: #10b981;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #1a2835;
margin: 20px 0;
}
p {
margin: 16px 0;
}
.verification-code {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
text-align: center;
margin: 20px 0;
letter-spacing: 5px;
font-size: 24px;
font-weight: bold;
color: #10b981;
}
.info-box {
background-color: #1a2835;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
}
.warning {
color: #f59e0b;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Trustworthy Casino</h1>
</div>
<div class="content">
<h2>Hallo <span class="highlight">${username}</span>,</h2>
<p>vielen Dank für Ihre Registrierung bei Trustworthy Casino. Um Ihr Konto zu aktivieren und Zugang zu allen Funktionen zu erhalten, bestätigen Sie bitte Ihre E-Mail-Adresse.</p>
<div class="divider"></div>
<p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p>
<div style="text-align: center;">
<a href="${feUrl}/verify?email-token=${token}" class="button">E-Mail bestätigen</a>
</div>
<div class="info-box">
<p><span class="warning">Hinweis:</span> Der Bestätigungscode könnte nur 24 Stunden gültig sein und kann vielleicht auch nur einmal verwendet werden.</p>
</div>
<div class="divider"></div>
<p>Nach der Bestätigung Ihrer E-Mail-Adresse können Sie sofort mit dem Spielen beginnen und alle Vorteile Ihres Kontos nutzen.</p>
<p>Bei Fragen stehen wir Ihnen jederzeit zur Verfügung.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
</div>
<div class="footer">
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
<p>Falls Sie diese E-Mail nicht angefordert haben, ignorieren Sie diese bitte.</p>
</div>
</div>
</body>
</html>

View file

@ -1,128 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen bei Trustworthy Casino©</title>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
background-color: #f8fafc;
color: #64748b;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #0a1219;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
background-color: #1a2835;
padding: 20px;
text-align: center;
}
.header h1 {
color: #ffffff;
margin: 0;
font-size: 28px;
}
.content {
background-color: #121e27;
padding: 30px;
color: #ffffff;
}
.footer {
background-color: #1a2835;
color: #94a3b8;
padding: 20px;
text-align: center;
font-size: 0.8em;
}
.button {
display: inline-block;
background-color: #10b981;
color: #ffffff;
padding: 12px 24px;
margin: 20px 0;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
transition: background-color 0.3s;
}
.button:hover {
background-color: #059669;
}
h2 {
color: #ffffff;
padding-bottom: 10px;
display: inline-block;
}
ul {
padding-left: 20px;
margin: 20px 0;
}
li {
margin-bottom: 8px;
color: #94a3b8;
}
li::marker {
color: #34d399;
}
.highlight {
color: #10b981;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #1a2835;
margin: 20px 0;
}
p {
margin: 16px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Trustworthy Casino</h1>
</div>
<div class="content">
<h2>Hallo <span class="highlight">${username}</span>,</h2>
<p>Herzlich willkommen bei Trustworthy Casino! Wir freuen uns, Sie an Bord zu haben.</p>
<div class="divider"></div>
<p>Bei uns erwarten Sie:</p>
<ul>
<li>Spannende Casino-Spiele</li>
<li>Sichere Transaktionen</li>
<li>Exklusive Boni und Aktionen</li>
</ul>
<div class="divider"></div>
<p>Melden Sie sich jetzt an und beginnen Sie Ihr Spielerlebnis!</p>
<div style="text-align: center;">
<a href="${feUrl}/home" class="button">Jetzt Spielen</a>
</div>
<p>Bei Fragen stehen wir Ihnen jederzeit zur Verfügung.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
</div>
<div class="footer">
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
</div>
</div>
</body>
</html>

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);
}
}

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