diff --git a/.metadata/.lock_info b/.metadata/.lock_info index c93b8fc..0ec633b 100644 --- a/.metadata/.lock_info +++ b/.metadata/.lock_info @@ -1,5 +1,5 @@ -#Mon Mar 02 07:06:09 CET 2026 +#Tue Mar 03 07:39:34 CET 2026 display=\:0 host=Mario-Linux -process-id=8641 +process-id=8529 user=mario diff --git a/.metadata/.log b/.metadata/.log index 2a0edce..90ec5c1 100644 --- a/.metadata/.log +++ b/.metadata/.log @@ -588,3 +588,140 @@ java.lang.InterruptedException at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) +!SESSION 2026-03-02 18:52:12.395 ----------------------------------------------- +eclipse.buildId=4.38.0.20251204-0849 +java.version=21.0.9 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-02 18:52:17.211 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-02 18:52:30.891 +!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml + +!ENTRY org.eclipse.ui 2 0 2026-03-02 18:52:31.083 +!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points. +!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-02 18:52:31.083 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.ui 2 0 2026-03-02 18:52:31.215 +!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points. +!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-02 18:52:31.215 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.jface 2 0 2026-03-02 19:47:48.567 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-02 19:47:48.567 +!MESSAGE A conflict occurred for CTRL+SHIFT+T: +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, + Open a type in a Java editor, + Category(org.eclipse.ui.category.navigate,Navigate,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace, + , + Category(org.eclipse.lsp4e.category,Language Servers,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) + +!ENTRY org.eclipse.debug.core 4 125 2026-03-02 20:36:48.515 +!MESSAGE Error logged from Debug Core: +!STACK 0 +java.io.IOException: Stream closed + at java.base/java.io.BufferedInputStream.getBufIfOpen(BufferedInputStream.java:188) + at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:343) + at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420) + at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:405) + at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95) + at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235) + at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211) + at java.base/java.lang.Thread.run(Thread.java:1583) +!SESSION 2026-03-03 07:39:25.427 ----------------------------------------------- +eclipse.buildId=4.38.0.20251204-0849 +java.version=21.0.9 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-03 07:39:30.215 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-03 07:39:35.273 +!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml + +!ENTRY org.eclipse.ui 2 0 2026-03-03 07:39:35.456 +!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points. +!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-03 07:39:35.456 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.ui 2 0 2026-03-03 07:39:35.586 +!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points. +!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-03 07:39:35.586 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.jface 2 0 2026-03-03 07:56:04.643 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-03 07:56:04.643 +!MESSAGE A conflict occurred for CTRL+SHIFT+T: +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, + Open a type in a Java editor, + Category(org.eclipse.ui.category.navigate,Navigate,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace, + , + Category(org.eclipse.lsp4e.category,Language Servers,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-03 15:57:21.578 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-03 15:59:16.948 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-03 16:03:19.413 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) diff --git a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json index bed5aab..67bd296 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json +++ b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json @@ -1,24 +1,7 @@ [ { - "version" : "9.5.0-20260301003351+0000", - "buildTime" : "20260301003351+0000", - "commitId" : "f5df27307ea67cc4fa298aa1a9e1fc01c77458d2", - "current" : false, - "snapshot" : true, - "nightly" : true, - "releaseNightly" : false, - "activeRc" : false, - "rcFor" : "", - "milestoneFor" : "", - "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260301003351+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260301003351+0000-bin.zip.sha256", - "checksum" : "ab9b82bcdaca040e4172b6fa10e3fcd3debaa377315760f6035c29d7837fad30", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260301003351+0000-wrapper.jar.sha256", - "wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8" -}, { - "version" : "9.4.0-20260228022640+0000", - "buildTime" : "20260228022640+0000", - "commitId" : "49c4c929c0d007cf3106e4ab65e91c9591376ac8", + "version" : "9.4.0-20260302013914+0000", + "buildTime" : "20260302013914+0000", + "commitId" : "3c885266535b1a7076dbecff4aac4830bd85a74b", "current" : false, "snapshot" : true, "nightly" : false, @@ -27,11 +10,28 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260228022640+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260228022640+0000-bin.zip.sha256", - "checksum" : "c8fe5e81a89bb9d72aa318f351af7f8cd6e205cb92708775082ea3b4be6e3e18", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260228022640+0000-wrapper.jar.sha256", + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260302013914+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260302013914+0000-bin.zip.sha256", + "checksum" : "1e225dd94668c5a4d5889c63d8a7e6d48b9750b68cc24d22afe407d56583fc5f", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.0-20260302013914+0000-wrapper.jar.sha256", "wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" +}, { + "version" : "9.5.0-20260302000223+0000", + "buildTime" : "20260302000223+0000", + "commitId" : "95405f4d8f3894f368fd90697eebd275f3d9a22d", + "current" : false, + "snapshot" : true, + "nightly" : true, + "releaseNightly" : false, + "activeRc" : false, + "rcFor" : "", + "milestoneFor" : "", + "broken" : false, + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260302000223+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260302000223+0000-bin.zip.sha256", + "checksum" : "434486e384ac04fbe16ae2bda2f4c9373cfe9f620aff4f5cc9c51f06e6f001ab", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260302000223+0000-wrapper.jar.sha256", + "wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8" }, { "version" : "9.4.0-rc-2", "buildTime" : "20260227092055+0000", diff --git a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame index f28d608..f246bb2 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame +++ b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame @@ -1,5 +1,5 @@ # -#Sun Mar 01 19:35:38 CET 2026 +#Mon Mar 02 22:58:56 CET 2026 buildDir=build buildScriptPath=build.gradle.kts classpath=\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\t\n\t\n\n\n diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.indexes/e4/b9/db/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.indexes/e4/b9/db/history.index index e10b957..f4e62ac 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.indexes/e4/b9/db/history.index and b/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.indexes/e4/b9/db/history.index differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.markers b/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.markers index 6502027..9e0b29f 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.markers and b/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.markers differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources index 3d5bec0..922a8b4 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources and b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources differ diff --git a/.metadata/.plugins/org.eclipse.debug.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.debug.ui/dialog_settings.xml index 8a453e0..12bf2f5 100644 --- a/.metadata/.plugins/org.eclipse.debug.ui/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.debug.ui/dialog_settings.xml @@ -8,4 +8,9 @@ +
+ + + +
diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi index f304e75..19119f1 100644 --- a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi +++ b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi @@ -1,8 +1,8 @@ - - + + activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration - + @@ -11,9 +11,9 @@ topLevel shellMaximized - - - + + + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -81,110 +81,117 @@ persp.editorOnboardingCommand:Show Key Assist$$$Shift+Ctrl+L persp.editorOnboardingCommand:New$$$Ctrl+N persp.editorOnboardingCommand:Open Type$$$Shift+Ctrl+T - - - + + + org.eclipse.e4.primaryNavigationStack - + active + noFocus + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - - + + View categoryTag:Git - - - - + + + + org.eclipse.e4.secondaryNavigationStack - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Java - + View categoryTag:Ant - + org.eclipse.e4.secondaryDataStack Gradle Oomph - + Version Control (Team) + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Gradle - + View categoryTag:Gradle - + + View + categoryTag:Version Control (Team) + + View categoryTag:Oomph NoRestore @@ -193,7 +200,7 @@ - + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -220,15 +227,8 @@ persp.viewSC:org.eclipse.ui.views.ProblemView persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer persp.viewSC:org.eclipse.pde.runtime.LogView - persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.debug.ui/icons/full/onboarding_debug_persp.svg - persp.editorOnboardingText:Go hunt your bugs here. persp.actionSet:org.eclipse.debug.ui.breakpointActionSet persp.showIn:org.eclipse.ui.navigator.ProjectExplorer - persp.editorOnboardingCommand:Find Actions$$$Ctrl+3 - persp.editorOnboardingCommand:Step Into$$$F5 - persp.editorOnboardingCommand:Step Over$$$F6 - persp.editorOnboardingCommand:Step Return$$$F7 - persp.editorOnboardingCommand:Resume$$$F8 persp.perspSC:org.eclipse.jdt.ui.JavaPerspective persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective persp.actionSet:org.eclipse.jdt.ui.JavaActionSet @@ -242,100 +242,107 @@ persp.showIn:org.eclipse.egit.ui.RepositoriesView persp.viewSC:org.eclipse.jdt.junit.ResultView persp.viewSC:org.eclipse.ant.ui.views.AntView - - + persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.debug.ui/icons/full/onboarding_debug_persp.svg + persp.editorOnboardingText:Go hunt your bugs here. + persp.editorOnboardingCommand:Find Actions$$$Ctrl+3 + persp.editorOnboardingCommand:Step Into$$$F5 + persp.editorOnboardingCommand:Step Over$$$F6 + persp.editorOnboardingCommand:Step Return$$$F7 + persp.editorOnboardingCommand:Resume$$$F8 + + org.eclipse.e4.primaryNavigationStack - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java - - - - + + + + org.eclipse.e4.secondaryNavigationStack - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Ant - - + + View categoryTag:General active - + View categoryTag:General - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Debug - + View categoryTag:General @@ -344,2620 +351,2661 @@ - - + + View categoryTag:Help - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:Help - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Help - - + + EditorStack org.eclipse.e4.primaryDataStack - active - noFocus - - + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + + + Editor removeOnHide org.eclipse.jdt.ui.CompilationUnitEditor - active - + View categoryTag:Java - + active + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:General - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Terminal - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Java - + View categoryTag:Ant - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + + + + + View + categoryTag:Debug + busy + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Debug + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Version Control (Team) + + ViewMenu + menuContribution:menu + + + + View categoryTag:Oomph NoRestore - + ViewMenu menuContribution:menu - + - - - - - View - categoryTag:Debug - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Debug - - - - - - View - categoryTag:Debug - - ViewMenu - menuContribution:menu - - - - - - - - View - categoryTag:Debug - - ViewMenu - menuContribution:menu - - - - - - - - View - categoryTag:Debug - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - - View - categoryTag:Debug - - ViewMenu - menuContribution:menu - - - - - + + toolbarSeparator - + - + Draggable - + - + toolbarSeparator - + - + Draggable - - + + - + toolbarSeparator - + - + Draggable - + Draggable - + Draggable - + Draggable - + toolbarSeparator - + - + Draggable - + - + Draggable - + toolbarSeparator - + - + toolbarSeparator - + - + Draggable - + stretch SHOW_RESTORE_MENU - + Draggable HIDEABLE SHOW_RESTORE_MENU - - + + stretch - + Draggable - + Draggable - - + + TrimStack Draggable - + TrimStack Draggable - - + + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - - - - - - - - - - - - - + + + + + + + + + + + + + platform:gtk - - - - + + + + platform:gtk - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - + + + - - - - - + + + + + - - + + - - - + + + - - - + + + - - - - - - - - + + + + + + + + platform:gtk - - - - - + + + + + - - + + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - + + - - - - + + + + - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Editor removeOnHide - + View categoryTag:Ant - + View categoryTag:Gradle - + View categoryTag:Gradle - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git NoRestore - + View categoryTag:Git - + View categoryTag:Help - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Oomph - + View categoryTag:Oomph NoRestore - + View categoryTag:Plug-in Development - + View categoryTag:General - + View categoryTag:Version Control (Team) - + View categoryTag:Version Control (Team) - + View categoryTag:Terminal - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Other - + View categoryTag:Other - + View categoryTag:Other - - + + glue move_after:PerspectiveSpacer SHOW_RESTORE_MENU - + move_after:Spacer Glue HIDEABLE SHOW_RESTORE_MENU - + glue move_after:SearchField SHOW_RESTORE_MENU - - - - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index index e2feec1..c56ea72 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index and b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/9341915.index b/.metadata/.plugins/org.eclipse.jdt.core/9341915.index index 2364e87..259107b 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/9341915.index and b/.metadata/.plugins/org.eclipse.jdt.core/9341915.index differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt index 35c3593..d5cafff 100644 --- a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt +++ b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt @@ -1,61 +1,61 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core -690321491.index +2318770678.index 1872440599.index 2240786275.index 4150628576.index 2655170954.index 4195864863.index 2982788279.index -2609856074.index 2626965509.index +2609856074.index 2769879155.index 4134502745.index 2817101718.index 4158338144.index 519552992.index -2181028596.index 2503368578.index +2181028596.index 1453089870.index 2593736024.index -721517855.index 815902026.index 3718169413.index +721517855.index 96642630.index 2488355463.index 1446719945.index -2891161224.index 1118739196.index +2891161224.index 2047888269.index -3972616808.index +2390245932.index 1205982295.index 1914043487.index -808711116.index 3154281632.index -2390245932.index +3972616808.index +808711116.index 2191830568.index 1653061733.index 2586591901.index 2609698604.index 3882180612.index 3758865325.index -2070370209.index 2332037983.index +2070370209.index 1732769785.index 2838468603.index +1436262503.index 2668411497.index -3662169204.index 2927822381.index 2398089967.index 225562445.index -1436262503.index +3662169204.index 1295630681.index 3135354350.index 3602551868.index -363836152.index 504781245.index +363836152.index 2633787677.index -1455171009.index 2725629017.index +1455171009.index 3552156823.index 4123041097.index 1865797976.index @@ -101,5 +101,5 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 198314732.index 1324521365.index 1633924572.index -2318770678.index +690321491.index 1256436118.index diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml index 172f389..e7f3c22 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml @@ -1,5 +1,5 @@ - + diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/0.log b/.metadata/.plugins/org.eclipse.m2e.logback/0.log index a5e0966..b3a77f1 100644 --- a/.metadata/.plugins/org.eclipse.m2e.logback/0.log +++ b/.metadata/.plugins/org.eclipse.m2e.logback/0.log @@ -2,3 +2,5 @@ 2026-03-01 18:42:13,387 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-01 19:35:44,100 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-02 07:06:13,818 [Worker-6: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. +2026-03-02 18:52:34,442 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update. +2026-03-03 07:39:38,191 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. diff --git a/.metadata/version.ini b/.metadata/version.ini index 2db3de6..78fac1f 100644 --- a/.metadata/version.ini +++ b/.metadata/version.ini @@ -1,3 +1,3 @@ -#Mon Mar 02 07:06:09 CET 2026 +#Tue Mar 03 07:39:34 CET 2026 org.eclipse.core.runtime=2 org.eclipse.platform=4.38.0.v20251201-0920 diff --git a/bilder/Vorlagen/Gemini_Generated_Image_eueluweueluweuel.png b/bilder/Vorlagen/Gemini_Generated_Image_eueluweueluweuel.png new file mode 100644 index 0000000..d827723 Binary files /dev/null and b/bilder/Vorlagen/Gemini_Generated_Image_eueluweueluweuel.png differ diff --git a/bilder/Vorlagen/Gemini_Generated_Image_swoht3swoht3swoh.png b/bilder/Vorlagen/Gemini_Generated_Image_swoht3swoht3swoh.png new file mode 100644 index 0000000..c148bc5 Binary files /dev/null and b/bilder/Vorlagen/Gemini_Generated_Image_swoht3swoht3swoh.png differ diff --git a/bilder/Vorlagen/icon.png b/bilder/Vorlagen/icon.png new file mode 100644 index 0000000..d1122c6 Binary files /dev/null and b/bilder/Vorlagen/icon.png differ diff --git a/bilder/Vorlagen/logo.png b/bilder/Vorlagen/logo.png new file mode 100644 index 0000000..f870be6 Binary files /dev/null and b/bilder/Vorlagen/logo.png differ diff --git a/bilder/Vorlagen/logo_old.png b/bilder/Vorlagen/logo_old.png new file mode 100644 index 0000000..73b8402 Binary files /dev/null and b/bilder/Vorlagen/logo_old.png differ diff --git a/bilder/icon.png b/bilder/icon.png new file mode 100644 index 0000000..1a16471 Binary files /dev/null and b/bilder/icon.png differ diff --git a/bilder/icon.xcf b/bilder/icon.xcf new file mode 100644 index 0000000..e166211 Binary files /dev/null and b/bilder/icon.xcf differ diff --git a/bilder/icon_transparent.png b/bilder/icon_transparent.png new file mode 100644 index 0000000..da7b7fd Binary files /dev/null and b/bilder/icon_transparent.png differ diff --git a/bilder/icon_transparent.xcf b/bilder/icon_transparent.xcf new file mode 100644 index 0000000..62bba3d Binary files /dev/null and b/bilder/icon_transparent.xcf differ diff --git a/bilder/logo.png b/bilder/logo.png new file mode 100644 index 0000000..69a4660 Binary files /dev/null and b/bilder/logo.png differ diff --git a/bilder/logo.xcf b/bilder/logo.xcf new file mode 100644 index 0000000..21d2754 Binary files /dev/null and b/bilder/logo.xcf differ diff --git a/bilder/logo_transparent.png b/bilder/logo_transparent.png new file mode 100644 index 0000000..ff1aafe Binary files /dev/null and b/bilder/logo_transparent.png differ diff --git a/bilder/logo_transparent.xcf b/bilder/logo_transparent.xcf new file mode 100644 index 0000000..e4c3a14 Binary files /dev/null and b/bilder/logo_transparent.xcf differ diff --git a/bilder/lvl1.png b/bilder/lvl1.png new file mode 100644 index 0000000..3bde9b7 Binary files /dev/null and b/bilder/lvl1.png differ diff --git a/bilder/lvl1.xcf b/bilder/lvl1.xcf new file mode 100644 index 0000000..153b39e Binary files /dev/null and b/bilder/lvl1.xcf differ diff --git a/bilder/lvl2.png b/bilder/lvl2.png new file mode 100644 index 0000000..bcf3a9d Binary files /dev/null and b/bilder/lvl2.png differ diff --git a/bilder/lvl2.xcf b/bilder/lvl2.xcf new file mode 100644 index 0000000..05d2425 Binary files /dev/null and b/bilder/lvl2.xcf differ diff --git a/bilder/lvl3.png b/bilder/lvl3.png new file mode 100644 index 0000000..c9a3e29 Binary files /dev/null and b/bilder/lvl3.png differ diff --git a/bilder/lvl3.xcf b/bilder/lvl3.xcf new file mode 100644 index 0000000..9ef4ef5 Binary files /dev/null and b/bilder/lvl3.xcf differ diff --git a/bilder/lvl4.png b/bilder/lvl4.png new file mode 100644 index 0000000..88ba411 Binary files /dev/null and b/bilder/lvl4.png differ diff --git a/bilder/lvl4.xcf b/bilder/lvl4.xcf new file mode 100644 index 0000000..3a61b5f Binary files /dev/null and b/bilder/lvl4.xcf differ diff --git a/bilder/lvl5.png b/bilder/lvl5.png new file mode 100644 index 0000000..72e16e4 Binary files /dev/null and b/bilder/lvl5.png differ diff --git a/bilder/lvl5.xcf b/bilder/lvl5.xcf new file mode 100644 index 0000000..1a41774 Binary files /dev/null and b/bilder/lvl5.xcf differ diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppe.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppe.java index 2331da5..97c719f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppe.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppe.java @@ -14,6 +14,7 @@ public class AufgabenGruppe { private List aufgaben; private List strafen; private List sperren; + private List finisher; private String bild; private long subscriberCount; private boolean subscribed; @@ -45,6 +46,9 @@ public class AufgabenGruppe { public List getSperren() { return sperren; } public void setSperren(List sperren) { this.sperren = sperren; } + public List getFinisher() { return finisher; } + public void setFinisher(List finisher) { this.finisher = finisher; } + public String getBild() { return bild; } public void setBild(String bild) { this.bild = bild; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/Finisher.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/Finisher.java new file mode 100644 index 0000000..93dc1c9 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/Finisher.java @@ -0,0 +1,47 @@ +package de.oaa.xxx.aufgaben; + +import de.oaa.xxx.session.GeschlechtEnum; + +import java.util.List; +import java.util.UUID; + +public class Finisher { + + private UUID finisherId; + private String kurzText; + private String text; + private GeschlechtEnum geschlecht; + private List benoetigtAktiv; + private List benoetigtPassiv; + private List benoetigteToys; + private UUID gruppeId; + + public UUID getFinisherId() { return finisherId; } + public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; } + + public String getKurzText() { return kurzText; } + public void setKurzText(String kurzText) { this.kurzText = kurzText; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public GeschlechtEnum getGeschlecht() { return geschlecht; } + public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; } + + public List getBenoetigtAktiv() { return benoetigtAktiv; } + public void setBenoetigtAktiv(List benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; } + + public List getBenoetigtPassiv() { return benoetigtPassiv; } + public void setBenoetigtPassiv(List benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; } + + public List getBenoetigteToys() { return benoetigteToys; } + public void setBenoetigteToys(List benoetigteToys) { this.benoetigteToys = benoetigteToys; } + + public UUID getGruppeId() { return gruppeId; } + public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; } + + @Override + public String toString() { + return "Finisher[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + ", gruppeId=" + gruppeId + "]"; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/AufgabenGruppeController.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/AufgabenGruppeController.java index b139f4f..0316ec9 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/AufgabenGruppeController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/AufgabenGruppeController.java @@ -5,11 +5,13 @@ import de.oaa.xxx.aufgaben.AufgabenGruppeList; import de.oaa.xxx.aufgaben.AufgabenGruppePage; import de.oaa.xxx.aufgaben.entity.AufgabeEntity; import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; +import de.oaa.xxx.aufgaben.entity.FinisherEntity; import de.oaa.xxx.aufgaben.entity.SperreEntity; import de.oaa.xxx.aufgaben.entity.StrafeEntity; import de.oaa.xxx.aufgaben.entity.ToyEntity; import de.oaa.xxx.aufgaben.repository.AufgabeRepository; import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository; +import de.oaa.xxx.aufgaben.repository.FinisherRepository; import de.oaa.xxx.aufgaben.repository.GruppenAboRepository; import de.oaa.xxx.aufgaben.repository.SperreRepository; import de.oaa.xxx.aufgaben.repository.StrafeRepository; @@ -57,6 +59,7 @@ public class AufgabenGruppeController { private final AufgabeRepository aufgabeRepository; private final StrafeRepository strafeRepository; private final SperreRepository sperreRepository; + private final FinisherRepository finisherRepository; private final UserRepository userRepository; private final GruppenAboRepository aboRepository; private final ToyRepository toyRepository; @@ -65,6 +68,7 @@ public class AufgabenGruppeController { AufgabeRepository aufgabeRepository, StrafeRepository strafeRepository, SperreRepository sperreRepository, + FinisherRepository finisherRepository, UserRepository userRepository, GruppenAboRepository aboRepository, ToyRepository toyRepository) { @@ -72,6 +76,7 @@ public class AufgabenGruppeController { this.aufgabeRepository = aufgabeRepository; this.strafeRepository = strafeRepository; this.sperreRepository = sperreRepository; + this.finisherRepository = finisherRepository; this.userRepository = userRepository; this.aboRepository = aboRepository; this.toyRepository = toyRepository; @@ -198,6 +203,7 @@ public class AufgabenGruppeController { source.getAufgaben().forEach(a -> { if (a.getBenoetigteToys() != null) allSourceToys.addAll(a.getBenoetigteToys()); }); source.getStrafen().forEach(s -> { if (s.getBenoetigteToys() != null) allSourceToys.addAll(s.getBenoetigteToys()); }); source.getSperren().forEach(sp -> { if (sp.getBenoetigteToys() != null) allSourceToys.addAll(sp.getBenoetigteToys()); }); + source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.getBenoetigteToys()); }); Map toyMapping = new HashMap<>(); for (ToyEntity sourceToy : allSourceToys) { @@ -274,6 +280,19 @@ public class AufgabenGruppeController { sperreRepository.save(spc); } + for (FinisherEntity f : source.getFinisher()) { + FinisherEntity fc = new FinisherEntity(); + fc.setFinisherId(UUID.randomUUID()); + fc.setAufgabenGruppe(copy); + fc.setKurzText(f.getKurzText()); + fc.setText(f.getText()); + fc.setGeschlecht(f.getGeschlecht()); + fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null); + fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null); + fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping)); + finisherRepository.save(fc); + } + return ResponseEntity.status(201).build(); } @@ -298,6 +317,7 @@ public class AufgabenGruppeController { aufgabeRepository.deleteAll(entity.getAufgaben()); strafeRepository.deleteAll(entity.getStrafen()); sperreRepository.deleteAll(entity.getSperren()); + finisherRepository.deleteAll(entity.getFinisher()); gruppeRepository.delete(entity); return ResponseEntity.accepted().build(); } catch (Exception e) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/FinisherController.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/FinisherController.java new file mode 100644 index 0000000..bce7007 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/controller/FinisherController.java @@ -0,0 +1,113 @@ +package de.oaa.xxx.aufgaben.controller; + +import de.oaa.xxx.aufgaben.Finisher; +import de.oaa.xxx.aufgaben.Toy; +import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; +import de.oaa.xxx.aufgaben.entity.FinisherEntity; +import de.oaa.xxx.aufgaben.entity.ToyEntity; +import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository; +import de.oaa.xxx.aufgaben.repository.FinisherRepository; +import de.oaa.xxx.aufgaben.repository.ToyRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/finisher") +@Transactional +public class FinisherController { + + private static final Logger LOGGER = LoggerFactory.getLogger(FinisherController.class); + + private final FinisherRepository finisherRepository; + private final AufgabenGruppeRepository gruppeRepository; + private final ToyRepository toyRepository; + + public FinisherController(FinisherRepository finisherRepository, + AufgabenGruppeRepository gruppeRepository, + ToyRepository toyRepository) { + this.finisherRepository = finisherRepository; + this.gruppeRepository = gruppeRepository; + this.toyRepository = toyRepository; + } + + @GetMapping("/{finisherId}") + public ResponseEntity get(@PathVariable UUID finisherId) { + return finisherRepository.findById(finisherId) + .map(entity -> ResponseEntity.ok(entity.toFinisher())) + .orElse(ResponseEntity.noContent().build()); + } + + @PostMapping + public ResponseEntity create(@RequestBody Finisher finisher) { + if (finisher.getKurzText() == null || finisher.getText() == null + || finisher.getGeschlecht() == null || finisher.getGruppeId() == null) { + return ResponseEntity.badRequest().build(); + } + AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(finisher.getGruppeId()).orElse(null); + if (gruppeEntity == null) { + return ResponseEntity.badRequest().build(); + } + if (gruppeEntity.getFinisher().size() >= 100) { + return ResponseEntity.status(409).build(); + } + List toys = resolveToys(finisher.getBenoetigteToys()); + FinisherEntity entity = FinisherEntity.create(finisher, gruppeEntity, toys); + finisherRepository.save(entity); + return ResponseEntity.created( + ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getFinisherId()).toUri() + ).build(); + } + + @PutMapping("/{finisherId}") + public ResponseEntity update(@PathVariable UUID finisherId, @RequestBody Finisher finisher) { + if (finisher.getKurzText() == null || finisher.getText() == null || finisher.getGeschlecht() == null) { + return ResponseEntity.badRequest().build(); + } + FinisherEntity entity = finisherRepository.findById(finisherId).orElse(null); + if (entity == null) return ResponseEntity.notFound().build(); + entity.setKurzText(finisher.getKurzText()); + entity.setText(finisher.getText()); + entity.setGeschlecht(finisher.getGeschlecht()); + entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); + entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); + entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys())); + finisherRepository.save(entity); + return ResponseEntity.ok().build(); + } + + @DeleteMapping + public ResponseEntity delete(@RequestBody Finisher finisher) { + try { + finisherRepository.findById(finisher.getFinisherId()).ifPresent(finisherRepository::delete); + return ResponseEntity.accepted().build(); + } catch (Exception exception) { + LOGGER.error(exception.getMessage(), exception); + return ResponseEntity.internalServerError().build(); + } + } + + private List resolveToys(List toys) { + if (toys == null || toys.isEmpty()) return new ArrayList<>(); + List ids = toys.stream() + .filter(t -> t.getToyId() != null) + .map(Toy::getToyId) + .toList(); + if (ids.isEmpty()) return new ArrayList<>(); + return toyRepository.findAllById(ids); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabeEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabeEntity.java index 4c050c4..0c98d77 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabeEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabeEntity.java @@ -84,6 +84,12 @@ public class AufgabeEntity { public List getBenoetigteToys() { return benoetigteToys; } public void setBenoetigteToys(List benoetigteToys) { this.benoetigteToys = benoetigteToys; } + @Override + public String toString() { + return "AufgabeEntity[id=" + aufgabeId + ", kurzText=" + kurzText + ", level=" + level + + ", sekunden=" + sekundenVon + "-" + sekundenBis + "]"; + } + public Aufgabe toAufgabe() { Aufgabe aufgabe = new Aufgabe(); aufgabe.setAufgabeId(aufgabeId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabenGruppeEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabenGruppeEntity.java index a1893a3..ed8af2e 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabenGruppeEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/AufgabenGruppeEntity.java @@ -39,6 +39,8 @@ public class AufgabenGruppeEntity { private List strafen; @OneToMany(mappedBy = "aufgabenGruppe") private List sperren; + @OneToMany(mappedBy = "aufgabenGruppe") + private List finisher; public UUID getGruppenId() { return gruppenId; } public void setGruppenId(UUID gruppenId) { this.gruppenId = gruppenId; } @@ -70,6 +72,15 @@ public class AufgabenGruppeEntity { public List getSperren() { return sperren; } public void setSperren(List sperren) { this.sperren = sperren; } + public List getFinisher() { return finisher; } + public void setFinisher(List finisher) { this.finisher = finisher; } + + @Override + public String toString() { + return "AufgabenGruppeEntity[gruppenId=" + gruppenId + ", name=" + name + ", userId=" + userId + + ", privat=" + privateGruppe + ", von=" + von + "]"; + } + public AufgabenGruppe toAufgabenGruppe() { AufgabenGruppe gruppe = new AufgabenGruppe(); gruppe.setGruppenId(gruppenId); @@ -82,6 +93,7 @@ public class AufgabenGruppeEntity { gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList()); gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList()); gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList()); + gruppe.setFinisher(finisher.stream().map(FinisherEntity::toFinisher).toList()); return gruppe; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FavoritEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FavoritEntity.java index 2b8e340..84634ba 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FavoritEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FavoritEntity.java @@ -29,6 +29,11 @@ public class FavoritEntity { public UUID getAufgabenGruppeId() { return aufgabenGruppeId; } public void setAufgabenGruppeId(UUID aufgabenGruppeId) { this.aufgabenGruppeId = aufgabenGruppeId; } + @Override + public String toString() { + return "FavoritEntity[favoritId=" + favoritId + ", userId=" + userId + ", gruppeId=" + aufgabenGruppeId + "]"; + } + public Favorit toFavorit() { Favorit favorit = new Favorit(); favorit.setAufgabenGruppeId(aufgabenGruppeId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FinisherEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FinisherEntity.java new file mode 100644 index 0000000..cb690b5 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/FinisherEntity.java @@ -0,0 +1,109 @@ +package de.oaa.xxx.aufgaben.entity; + +import de.oaa.xxx.aufgaben.Finisher; +import de.oaa.xxx.aufgaben.Werkzeug; +import de.oaa.xxx.session.GeschlechtEnum; +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "finisher") +public class FinisherEntity { + + @Id + @Column + private UUID finisherId; + @Column + private String kurzText; + @Column(columnDefinition = "TEXT") + private String text; + @Enumerated(EnumType.STRING) + @Column + private GeschlechtEnum geschlecht; + @ManyToOne + @JoinColumn(name = "gruppeId") + private AufgabenGruppeEntity aufgabenGruppe; + @Enumerated(EnumType.STRING) + @ElementCollection(targetClass = Werkzeug.class) + @CollectionTable(name = "finisher_benoetigtAktiv", joinColumns = @JoinColumn(name = "finisherId")) + @Column(name = "werkzeug") + private List benoetigtAktiv; + @Enumerated(EnumType.STRING) + @ElementCollection(targetClass = Werkzeug.class) + @CollectionTable(name = "finisher_benoetigtPassiv", joinColumns = @JoinColumn(name = "finisherId")) + @Column(name = "werkzeug") + private List benoetigtPassiv; + @ManyToMany(cascade = CascadeType.DETACH) + @JoinTable(name = "finisherToy", joinColumns = {@JoinColumn(name = "finisherId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")}) + private List benoetigteToys; + + public UUID getFinisherId() { return finisherId; } + public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; } + + public String getKurzText() { return kurzText; } + public void setKurzText(String kurzText) { this.kurzText = kurzText; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public GeschlechtEnum getGeschlecht() { return geschlecht; } + public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; } + + public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; } + public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; } + + public List getBenoetigtAktiv() { return benoetigtAktiv; } + public void setBenoetigtAktiv(List benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; } + + public List getBenoetigtPassiv() { return benoetigtPassiv; } + public void setBenoetigtPassiv(List benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; } + + public List getBenoetigteToys() { return benoetigteToys; } + public void setBenoetigteToys(List benoetigteToys) { this.benoetigteToys = benoetigteToys; } + + @Override + public String toString() { + return "FinisherEntity[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + "]"; + } + + public Finisher toFinisher() { + Finisher finisher = new Finisher(); + finisher.setFinisherId(finisherId); + finisher.setKurzText(kurzText); + finisher.setText(text); + finisher.setGeschlecht(geschlecht); + finisher.setBenoetigtAktiv(benoetigtAktiv); + finisher.setBenoetigtPassiv(benoetigtPassiv); + finisher.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList()); + finisher.setGruppeId(aufgabenGruppe.getGruppenId()); + return finisher; + } + + public static FinisherEntity create(Finisher finisher, AufgabenGruppeEntity aufgabenGruppeEntity, List toys) { + FinisherEntity entity = new FinisherEntity(); + entity.setFinisherId(UUID.randomUUID()); + entity.setAufgabenGruppe(aufgabenGruppeEntity); + entity.setKurzText(finisher.getKurzText()); + entity.setText(finisher.getText()); + entity.setGeschlecht(finisher.getGeschlecht()); + entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); + entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); + entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>()); + return entity; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/GruppenAboEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/GruppenAboEntity.java index 9c38f11..2679e77 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/GruppenAboEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/GruppenAboEntity.java @@ -32,4 +32,10 @@ public class GruppenAboEntity { public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; } public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; } + + @Override + public String toString() { + return "GruppenAboEntity[aboId=" + aboId + ", userId=" + userId + + ", gruppe=" + (aufgabenGruppe != null ? aufgabenGruppe.getName() : null) + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/SperreEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/SperreEntity.java index 0a5186c..f56c350 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/SperreEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/SperreEntity.java @@ -76,6 +76,12 @@ public class SperreEntity { public List getBenoetigteToys() { return benoetigteToys; } public void setBenoetigteToys(List benoetigteToys) { this.benoetigteToys = benoetigteToys; } + @Override + public String toString() { + return "SperreEntity[id=" + sperreId + ", kurzText=" + kurzText + + ", minuten=" + minutenVon + "-" + minutenBis + ", fuer=" + sperreFuer + "]"; + } + public Sperre toSperre() { Sperre sperre = new Sperre(); sperre.setSperreId(sperreId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/StrafeEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/StrafeEntity.java index 3ef2955..66c91c7 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/StrafeEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/StrafeEntity.java @@ -84,6 +84,12 @@ public class StrafeEntity { public List getBenoetigteToys() { return benoetigteToys; } public void setBenoetigteToys(List benoetigteToys) { this.benoetigteToys = benoetigteToys; } + @Override + public String toString() { + return "StrafeEntity[id=" + strafeId + ", kurzText=" + kurzText + ", level=" + level + + ", sekunden=" + sekundenVon + "-" + sekundenBis + "]"; + } + public Strafe toStrafe() { Strafe strafe = new Strafe(); strafe.setStrafeId(strafeId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/ToyEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/ToyEntity.java index 6fc2025..95e2898 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/ToyEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/entity/ToyEntity.java @@ -42,6 +42,11 @@ public class ToyEntity { public byte[] getBild() { return bild; } public void setBild(byte[] bild) { this.bild = bild; } + @Override + public String toString() { + return "ToyEntity[toyId=" + toyId + ", name=" + name + ", userId=" + userId + "]"; + } + public Toy toToy() { Toy toy = new Toy(); toy.setToyId(toyId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/AufgabeRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/AufgabeRepository.java index 2ba62ef..28d2e5f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/AufgabeRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/AufgabeRepository.java @@ -1,9 +1,13 @@ package de.oaa.xxx.aufgaben.repository; import de.oaa.xxx.aufgaben.entity.AufgabeEntity; +import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; public interface AufgabeRepository extends JpaRepository { + + List findByAufgabenGruppeIn(List gruppen); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FavoritRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FavoritRepository.java index a92079e..48d07c4 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FavoritRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FavoritRepository.java @@ -11,4 +11,6 @@ public interface FavoritRepository extends JpaRepository { List findByUserId(UUID userId); List findByUserIdAndAufgabenGruppeId(UUID userId, UUID aufgabenGruppeId); + + void deleteByAufgabenGruppeId(UUID aufgabenGruppeId); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FinisherRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FinisherRepository.java new file mode 100644 index 0000000..ba8841c --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/FinisherRepository.java @@ -0,0 +1,9 @@ +package de.oaa.xxx.aufgaben.repository; + +import de.oaa.xxx.aufgaben.entity.FinisherEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface FinisherRepository extends JpaRepository { +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/SperreRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/SperreRepository.java index 68811a4..bac1cf7 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/SperreRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/SperreRepository.java @@ -1,9 +1,13 @@ package de.oaa.xxx.aufgaben.repository; +import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; import de.oaa.xxx.aufgaben.entity.SperreEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; public interface SperreRepository extends JpaRepository { + + List findByAufgabenGruppeIn(List gruppen); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/StrafeRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/StrafeRepository.java index 232e8b2..4d91cdb 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/StrafeRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/StrafeRepository.java @@ -1,9 +1,13 @@ package de.oaa.xxx.aufgaben.repository; +import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; import de.oaa.xxx.aufgaben.entity.StrafeEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; public interface StrafeRepository extends JpaRepository { + + List findByAufgabenGruppeIn(List gruppen); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/ToyRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/ToyRepository.java index 4f92fe8..8e8eb53 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/ToyRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/repository/ToyRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -16,6 +17,8 @@ public interface ToyRepository extends JpaRepository { Page findByUserId(UUID userId, Pageable pageable); + List findByUserId(UUID userId); + boolean existsByNameIgnoreCaseAndUserIdIsNull(String name); boolean existsByNameIgnoreCaseAndUserId(String name, UUID userId); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java index a5c52b1..f25baf8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java @@ -36,18 +36,40 @@ public class SecurityConfig { .requestMatchers(AntPathRequestMatcher.antMatcher("/toys.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/aufgaben.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/entdecken.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/profile.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/infovanilla.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/infobdsm.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/infochastity.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionvanilla.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsm.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastity.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtasks.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtoys.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmingame.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/personen-suchen.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/freunde.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/nachrichten.html")).authenticated() + .requestMatchers(AntPathRequestMatcher.antMatcher("/benutzer.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/images/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/favicon.ico")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher("/*.png")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher("/*.jpg")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher("/*.svg")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher("/*.webp")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login/publickey")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login/logout")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/user")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/registration")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/registration")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation/**")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/request")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/confirm")).permitAll() + .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/email-change/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/filler")).permitAll() .anyRequest().authenticated() ) diff --git a/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java new file mode 100644 index 0000000..cd5cbfb --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java @@ -0,0 +1,119 @@ +package de.oaa.xxx.emailchange; + +import de.oaa.xxx.mail.Email; +import de.oaa.xxx.mail.MailService; +import de.oaa.xxx.mail.MailTemplateService; +import de.oaa.xxx.registration.RegistrationRepository; +import de.oaa.xxx.user.UserRepository; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.security.Principal; +import java.util.UUID; + +@RestController +@RequestMapping("/email-change") +public class EmailChangeController { + + private static final Logger LOGGER = LoggerFactory.getLogger(EmailChangeController.class); + + @Value("${app.base-url:http://localhost:8080}") + private String baseUrl; + + private final EmailChangeRepository emailChangeRepository; + private final UserRepository userRepository; + private final RegistrationRepository registrationRepository; + private final MailService mailService; + private final MailTemplateService mailTemplateService; + + public EmailChangeController(EmailChangeRepository emailChangeRepository, + UserRepository userRepository, + RegistrationRepository registrationRepository, + MailService mailService, + MailTemplateService mailTemplateService) { + this.emailChangeRepository = emailChangeRepository; + this.userRepository = userRepository; + this.registrationRepository = registrationRepository; + this.mailService = mailService; + this.mailTemplateService = mailTemplateService; + } + + record EmailChangeRequest(String newEmail) {} + + @PostMapping + public ResponseEntity requestChange(@RequestBody EmailChangeRequest request, Principal principal) { + String currentEmail = principal.getName(); + String newEmail = request.newEmail(); + + if (userRepository.findByEmail(newEmail).isPresent() + || registrationRepository.findByEmail(newEmail).isPresent()) { + return ResponseEntity.status(409).build(); + } + + // Remove any pending request for this user + emailChangeRepository.findByUserEmail(currentEmail) + .ifPresent(emailChangeRepository::delete); + + var user = userRepository.findByEmail(currentEmail); + if (user.isEmpty()) return ResponseEntity.status(401).build(); + + EmailChangeEntity entity = EmailChangeEntity.create(currentEmail, newEmail); + emailChangeRepository.save(entity); + + Email email = new Email(); + email.setTitel("Bitte bestätige deine neue E-Mail-Adresse"); + email.setEmailAdresse(newEmail); + String confirmLink = baseUrl + "/email-change/" + entity.getTokenId().toString(); + email.setText(mailTemplateService.buildEmailChangeMail(user.get().getName(), confirmLink, newEmail)); + + if (!mailService.send(email)) { + emailChangeRepository.delete(entity); + return ResponseEntity.internalServerError().build(); + } + + return ResponseEntity.status(202).build(); + } + + @GetMapping("/{token}") + public void confirm(@PathVariable String token, HttpServletResponse response) throws IOException { + UUID tokenId; + try { + tokenId = UUID.fromString(token); + } catch (IllegalArgumentException e) { + response.sendRedirect("/login.html"); + return; + } + + var entity = emailChangeRepository.findById(tokenId); + if (entity.isEmpty()) { + response.sendRedirect("/login.html"); + return; + } + + var user = userRepository.findByEmail(entity.get().getUserEmail()); + if (user.isPresent()) { + user.get().setEmail(entity.get().getNewEmail()); + userRepository.save(user.get()); + LOGGER.info("E-Mail geändert von {} zu {}", entity.get().getUserEmail(), entity.get().getNewEmail()); + } + + emailChangeRepository.delete(entity.get()); + + // Clear JWT cookie so user must log in with new email + ResponseCookie cookie = ResponseCookie.from("jwt", "") + .httpOnly(true) + .sameSite("Strict") + .path("/") + .maxAge(0) + .build(); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + response.sendRedirect("/login.html?emailChanged=1"); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeEntity.java new file mode 100644 index 0000000..dcf7161 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeEntity.java @@ -0,0 +1,53 @@ +package de.oaa.xxx.emailchange; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "email_change") +public class EmailChangeEntity { + + @Id + @Column + private UUID tokenId; + + @Column + private String userEmail; + + @Column + private String newEmail; + + @Column + private LocalDateTime createdAt; + + public UUID getTokenId() { return tokenId; } + public void setTokenId(UUID tokenId) { this.tokenId = tokenId; } + + public String getUserEmail() { return userEmail; } + public void setUserEmail(String userEmail) { this.userEmail = userEmail; } + + public String getNewEmail() { return newEmail; } + public void setNewEmail(String newEmail) { this.newEmail = newEmail; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + @Override + public String toString() { + return "EmailChangeEntity[tokenId=" + tokenId + ", userEmail=" + userEmail + ", newEmail=" + newEmail + ", createdAt=" + createdAt + "]"; + } + + public static EmailChangeEntity create(String userEmail, String newEmail) { + EmailChangeEntity entity = new EmailChangeEntity(); + entity.setTokenId(UUID.randomUUID()); + entity.setUserEmail(userEmail); + entity.setNewEmail(newEmail); + entity.setCreatedAt(LocalDateTime.now()); + return entity; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeRepository.java new file mode 100644 index 0000000..c941e81 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/emailchange/EmailChangeRepository.java @@ -0,0 +1,11 @@ +package de.oaa.xxx.emailchange; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface EmailChangeRepository extends JpaRepository { + + Optional findByUserEmail(String userEmail); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/mail/MailTemplateService.java b/xxxthegame/src/main/java/de/oaa/xxx/mail/MailTemplateService.java index 891ca8a..b1ef700 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/mail/MailTemplateService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/mail/MailTemplateService.java @@ -32,6 +32,90 @@ public class MailTemplateService { @Value("${app.theme.color-success:#2ecc71}") private String colorSuccess; + public String buildEmailChangeMail(String name, String confirmLink, String newEmail) { + return """ + + + +
+ +

XXX The Game

+ +

Moin %s,

+

Du hast eine Änderung deiner E-Mail-Adresse angefordert.

+

Klick auf den Button, um deine neue Adresse %s zu bestätigen:

+ + + +
+ +

+ Falls du diese Änderung nicht angefordert hast, kannst du diese E-Mail einfach ignorieren. +

+
+ + + """.formatted( + colorBg, colorText, + colorCard, colorSecondary, + colorPrimary, + colorText, name, + colorText, + colorText, colorPrimary, newEmail, + confirmLink, colorPrimary, + colorSecondary, + colorMuted + ); + } + + public String buildPasswordResetMail(String name, String resetLink) { + return """ + + + +
+ +

XXX The Game

+ +

Moin %s,

+

Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.

+

Klick auf den Button, um ein neues Passwort zu vergeben:

+ + + +
+ +

+ Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren. +

+
+ + + """.formatted( + colorBg, colorText, + colorCard, colorSecondary, + colorPrimary, + colorText, name, + colorText, + colorText, + resetLink, colorPrimary, + colorSecondary, + colorMuted + ); + } + public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) { return """ diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java new file mode 100644 index 0000000..8462c02 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java @@ -0,0 +1,3 @@ +package de.oaa.xxx.passwordreset; + +public record PasswordResetConfirm(String token, String passwordHash) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java new file mode 100644 index 0000000..0707d39 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java @@ -0,0 +1,77 @@ +package de.oaa.xxx.passwordreset; + +import de.oaa.xxx.mail.Email; +import de.oaa.xxx.mail.MailService; +import de.oaa.xxx.mail.MailTemplateService; +import de.oaa.xxx.user.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/password-reset") +public class PasswordResetController { + + private static final Logger LOGGER = LoggerFactory.getLogger(PasswordResetController.class); + + @Value("${app.base-url:http://localhost:8080}") + private String baseUrl; + + private final PasswordResetRepository passwordResetRepository; + private final UserRepository userRepository; + private final MailService mailService; + private final MailTemplateService mailTemplateService; + + public PasswordResetController(PasswordResetRepository passwordResetRepository, + UserRepository userRepository, + MailService mailService, + MailTemplateService mailTemplateService) { + this.passwordResetRepository = passwordResetRepository; + this.userRepository = userRepository; + this.mailService = mailService; + this.mailTemplateService = mailTemplateService; + } + + @PostMapping("/request") + public ResponseEntity request(@RequestBody PasswordResetRequest request) { + userRepository.findByEmail(request.email()).ifPresent(user -> { + passwordResetRepository.findByEmail(request.email()) + .ifPresent(passwordResetRepository::delete); + PasswordResetEntity entity = PasswordResetEntity.create(request.email()); + passwordResetRepository.save(entity); + String resetLink = baseUrl + "/reset-password.html?token=" + entity.getTokenId(); + Email email = new Email(); + email.setTitel("Passwort zurücksetzen"); + email.setEmailAdresse(request.email()); + email.setText(mailTemplateService.buildPasswordResetMail(user.getName(), resetLink)); + mailService.send(email); + LOGGER.info("Passwort-Reset angefordert für: {}", request.email()); + }); + return ResponseEntity.status(202).build(); + } + + @PostMapping("/confirm") + public ResponseEntity confirm(@RequestBody PasswordResetConfirm confirm) { + UUID tokenId; + try { + tokenId = UUID.fromString(confirm.token()); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + var entity = passwordResetRepository.findById(tokenId); + if (entity.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> { + user.setPassword(confirm.passwordHash()); + userRepository.save(user); + LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail()); + }); + passwordResetRepository.delete(entity.get()); + return ResponseEntity.ok().build(); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetEntity.java new file mode 100644 index 0000000..bdeaa55 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetEntity.java @@ -0,0 +1,46 @@ +package de.oaa.xxx.passwordreset; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "password_reset") +public class PasswordResetEntity { + + @Id + @Column + private UUID tokenId; + + @Column(unique = true) + private String email; + + @Column + private LocalDateTime createdAt; + + public UUID getTokenId() { return tokenId; } + public void setTokenId(UUID tokenId) { this.tokenId = tokenId; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + @Override + public String toString() { + return "PasswordResetEntity[tokenId=" + tokenId + ", email=" + email + ", createdAt=" + createdAt + "]"; + } + + public static PasswordResetEntity create(String email) { + PasswordResetEntity entity = new PasswordResetEntity(); + entity.setTokenId(UUID.randomUUID()); + entity.setEmail(email); + entity.setCreatedAt(LocalDateTime.now()); + return entity; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRepository.java new file mode 100644 index 0000000..dcf0fe1 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRepository.java @@ -0,0 +1,10 @@ +package de.oaa.xxx.passwordreset; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface PasswordResetRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRequest.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRequest.java new file mode 100644 index 0000000..54c7edc --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetRequest.java @@ -0,0 +1,3 @@ +package de.oaa.xxx.passwordreset; + +public record PasswordResetRequest(String email) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java index 87fbcc1..c2c0f0b 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java @@ -44,6 +44,11 @@ public class RegistrationController { LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail()); return ResponseEntity.badRequest().build(); } + if (registrationRepository.findByName(registration.getName()).isPresent() + || userRepository.findByName(registration.getName()).isPresent()) { + LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName()); + return ResponseEntity.status(409).build(); + } RegistrationEntity entity = RegistrationEntity.create(registration); registrationRepository.save(entity); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationRepository.java index a40aedc..5aa763d 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationRepository.java @@ -8,4 +8,5 @@ import java.util.UUID; public interface RegistrationRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByName(String name); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/AktiveSperre.java b/xxxthegame/src/main/java/de/oaa/xxx/session/AktiveSperre.java index 5a2bfbe..32049e8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/AktiveSperre.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/AktiveSperre.java @@ -34,4 +34,10 @@ public class AktiveSperre { public String getReleaseText() { return releaseText; } public void setReleaseText(String releaseText) { this.releaseText = releaseText; } + + @Override + public String toString() { + return "AktiveSperre[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null) + + ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/AufgabeAnzeige.java b/xxxthegame/src/main/java/de/oaa/xxx/session/AufgabeAnzeige.java index fd89941..0b66ea3 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/AufgabeAnzeige.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/AufgabeAnzeige.java @@ -6,6 +6,7 @@ public class AufgabeAnzeige { private String aufgabeText; private Integer timer; private Callback callback; + private Integer level; public String getNameAktiverMitspieler() { return nameAktiverMitspieler; } public void setNameAktiverMitspieler(String nameAktiverMitspieler) { this.nameAktiverMitspieler = nameAktiverMitspieler; } @@ -18,4 +19,13 @@ public class AufgabeAnzeige { public Callback getCallback() { return callback; } public void setCallback(Callback callback) { this.callback = callback; } + + public Integer getLevel() { return level; } + public void setLevel(Integer level) { this.level = level; } + + @Override + public String toString() { + return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer + + ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/Callback.java b/xxxthegame/src/main/java/de/oaa/xxx/session/Callback.java index 020b805..a116d45 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/Callback.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/Callback.java @@ -8,4 +8,9 @@ public abstract class Callback { public UUID getSessionId() { return sessionId; } public void setSessionId(UUID sessionId) { this.sessionId = sessionId; } + + @Override + public String toString() { + return getClass().getSimpleName() + "[sessionId=" + sessionId + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/Mitspieler.java b/xxxthegame/src/main/java/de/oaa/xxx/session/Mitspieler.java index 2b53339..09ee82f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/Mitspieler.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/Mitspieler.java @@ -34,6 +34,12 @@ public class Mitspieler { return verfuegbareWerkzeuge.contains(werkzeug); } + @Override + public String toString() { + return "Mitspieler[id=" + id + ", name=" + name + ", geschlecht=" + geschlecht + + ", rollen=" + rollen + ", werkzeuge=" + verfuegbareWerkzeuge + "]"; + } + public boolean isPassenderSpielpartner(Mitspieler other) { if (!spieltMit.contains(other.getGeschlecht())) { return false; diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/Session.java b/xxxthegame/src/main/java/de/oaa/xxx/session/Session.java index b7696a1..fc8b8a8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/Session.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/Session.java @@ -1,5 +1,6 @@ package de.oaa.xxx.session; +import java.time.LocalDateTime; import java.util.UUID; public class Session { @@ -10,6 +11,10 @@ public class Session { private Integer wahrscheinlichkeitStrafe; private Integer aufgabenProLevel; private Double zeitfaktorZeitstrafen; + private Integer level; + private Integer aufgabenAufAktuellemLevel; + private LocalDateTime startZeit; + private LocalDateTime letzteAktivitaet; public UUID getSessionId() { return sessionId; } public void setSessionId(UUID sessionId) { this.sessionId = sessionId; } @@ -28,4 +33,24 @@ public class Session { public Double getZeitfaktorZeitstrafen() { return zeitfaktorZeitstrafen; } public void setZeitfaktorZeitstrafen(Double zeitfaktorZeitstrafen) { this.zeitfaktorZeitstrafen = zeitfaktorZeitstrafen; } + + public Integer getLevel() { return level; } + public void setLevel(Integer level) { this.level = level; } + + public Integer getAufgabenAufAktuellemLevel() { return aufgabenAufAktuellemLevel; } + public void setAufgabenAufAktuellemLevel(Integer aufgabenAufAktuellemLevel) { this.aufgabenAufAktuellemLevel = aufgabenAufAktuellemLevel; } + + public LocalDateTime getStartZeit() { return startZeit; } + public void setStartZeit(LocalDateTime startZeit) { this.startZeit = startZeit; } + + public LocalDateTime getLetzteAktivitaet() { return letzteAktivitaet; } + public void setLetzteAktivitaet(LocalDateTime letzteAktivitaet) { this.letzteAktivitaet = letzteAktivitaet; } + + @Override + public String toString() { + return "Session[sessionId=" + sessionId + ", userId=" + userId + + ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel + + ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%" + + ", zeitfaktor=" + zeitfaktorZeitstrafen + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/SessionDurchfuehren.java b/xxxthegame/src/main/java/de/oaa/xxx/session/SessionDurchfuehren.java index e216411..28bdeb6 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/SessionDurchfuehren.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/SessionDurchfuehren.java @@ -1,6 +1,12 @@ package de.oaa.xxx.session; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + import com.fasterxml.jackson.databind.ObjectMapper; + import de.oaa.xxx.session.aufgaben.Aufgabe; import de.oaa.xxx.session.aufgaben.AufgabenList; import de.oaa.xxx.session.aufgaben.Sperre; @@ -9,11 +15,6 @@ import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.session.sperre.SperreCallback; import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.stream.Collectors; - public class SessionDurchfuehren { private final AufgabenList aufgabenList; @@ -23,12 +24,12 @@ public class SessionDurchfuehren { private final Integer wahrscheinlichkeitSperre; private final Integer wahrscheinlichkeitStrafe; - private final Integer aufgabenProLevel; - private Integer level; - private Integer aufgabenAufAktuellemLevel; + private int aufgabenProLevel; + private int level; + private int aufgabenAufAktuellemLevel; public SessionDurchfuehren(SessionEntity entity) throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = new ObjectMapper(); aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class); entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler())); entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler))); @@ -36,13 +37,16 @@ public class SessionDurchfuehren { wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre(); wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe(); - aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5; - level = entity.getLevel() != null ? entity.getLevel() : 1; - aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0; + this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5; + this.level = entity.getLevel() != null ? entity.getLevel() : 1; + this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0; } public AufgabeAnzeige getNext() { checkLevel(); + if (level == 6) { + return null; + } AufgabeAnzeige anzeige = null; int nextInt = new Random().nextInt(1, 100); if (nextInt == 1) { @@ -67,9 +71,34 @@ public class SessionDurchfuehren { } return anzeige; } + + public void backToLvl5() { + this.level = 5; + this.aufgabenAufAktuellemLevel = 0; + } + + public List getFinisher() { + var list = new ArrayList(); + List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> { + mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> { + var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming); + aufgabenList.getFinisher().stream() + .filter(finisher -> geschlecht == finisher.getGeschlecht()) + .findAny() + .ifPresent(aufgabe -> { + AufgabeAnzeige anzeige = new AufgabeAnzeige(); + anzeige.setNameAktiverMitspieler(cumming.getName()); + anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), + cumming.getName(), partner != null ? partner.getName() : "")); + list.add(anzeige); + }); + }); + }); + return list; + } private void checkLevel() { - if (++aufgabenAufAktuellemLevel >= aufgabenProLevel && level < 5) { + if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) { aufgabenAufAktuellemLevel = 0; level++; } @@ -210,4 +239,12 @@ public class SessionDurchfuehren { .toList(); return list.isEmpty() ? null : list.get(new Random().nextInt(list.size())); } + + public int getAufgabenAufAktuellemLevel() { + return aufgabenAufAktuellemLevel; + } + + public int getLevel() { + return level; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Aufgabe.java b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Aufgabe.java index f1928d8..8dc0b97 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Aufgabe.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Aufgabe.java @@ -45,6 +45,12 @@ public class Aufgabe { public List getBenoetigtPassiv() { return benoetigtPassiv; } public void setBenoetigtPassiv(List benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; } + @Override + public String toString() { + return "Aufgabe[id=" + aufgabeId + ", kurzText=" + kurzText + ", level=" + level + + ", sekunden=" + sekundenVon + "-" + sekundenBis + ", gruppeId=" + gruppeId + "]"; + } + public boolean isAufgabePassend(int level, Mitspieler aktiv, Mitspieler passiv) { if (level != this.level && level - 1 != this.level) { return false; diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/AufgabenList.java b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/AufgabenList.java index 4414668..5f585fe 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/AufgabenList.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/AufgabenList.java @@ -7,6 +7,7 @@ public class AufgabenList { private List aufgaben; private List sperren; private List strafen; + private List finisher; public List getAufgaben() { return aufgaben; } public void setAufgaben(List aufgaben) { this.aufgaben = aufgaben; } @@ -17,11 +18,15 @@ public class AufgabenList { public List getStrafen() { return strafen; } public void setStrafen(List strafen) { this.strafen = strafen; } + public List getFinisher() { return finisher; } + public void setFinisher(List finisher) { this.finisher = finisher; } + public int size() { int size = 0; if (aufgaben != null) size += aufgaben.size(); if (sperren != null) size += sperren.size(); if (strafen != null) size += strafen.size(); + if (getFinisher() != null) size += getFinisher().size(); return size; } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Finisher.java b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Finisher.java new file mode 100644 index 0000000..041e78a --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Finisher.java @@ -0,0 +1,40 @@ +package de.oaa.xxx.session.aufgaben; + +import java.util.List; +import java.util.UUID; + +import de.oaa.xxx.session.GeschlechtEnum; +import de.oaa.xxx.session.Werkzeug; + +public class Finisher { + + private UUID finisherId; + private String kurzText; + private String text; + private GeschlechtEnum geschlecht; + private List benoetigtAktiv; + private List benoetigtPassiv; + + public UUID getFinisherId() { return finisherId; } + public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; } + + public String getKurzText() { return kurzText; } + public void setKurzText(String kurzText) { this.kurzText = kurzText; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public GeschlechtEnum getGeschlecht() { return geschlecht; } + public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; } + + public List getBenoetigtAktiv() { return benoetigtAktiv; } + public void setBenoetigtAktiv(List benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; } + + public List getBenoetigtPassiv() { return benoetigtPassiv; } + public void setBenoetigtPassiv(List benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; } + + @Override + public String toString() { + return "Finisher[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + "]"; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Sperre.java b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Sperre.java index bd4e533..16a4fc7 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Sperre.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/aufgaben/Sperre.java @@ -41,6 +41,12 @@ public class Sperre { public Integer getMinutenBis() { return minutenBis; } public void setMinutenBis(Integer minutenBis) { this.minutenBis = minutenBis; } + @Override + public String toString() { + return "Sperre[id=" + sperreId + ", kurzText=" + kurzText + + ", minuten=" + minutenVon + "-" + minutenBis + ", fuer=" + sperreFuer + ", gruppeId=" + gruppeId + "]"; + } + public boolean isAufgabePassend(Mitspieler passiv) { for (Werkzeug werkzeug : sperreFuer) { if (!passiv.isVerfuegbar(werkzeug)) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SessionController.java b/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SessionController.java index a048070..6a78f21 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SessionController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SessionController.java @@ -8,8 +8,10 @@ import de.oaa.xxx.session.SessionDurchfuehren; import de.oaa.xxx.session.aufgaben.AufgabenList; import de.oaa.xxx.session.entity.MitspielerEntity; import de.oaa.xxx.session.entity.SessionEntity; +import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.session.repository.SessionRepository; +import de.oaa.xxx.user.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -26,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; @RestController @@ -37,12 +40,17 @@ public class SessionController { private final SessionRepository sessionRepository; private final MitspielerRepository mitspielerRepository; + private final AktiveSperreRepository aktiveSperreRepository; + private final UserRepository userRepository; private final ObjectMapper objectMapper; public SessionController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository, + AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository, ObjectMapper objectMapper) { this.sessionRepository = sessionRepository; this.mitspielerRepository = mitspielerRepository; + this.aktiveSperreRepository = aktiveSperreRepository; + this.userRepository = userRepository; this.objectMapper = objectMapper; } @@ -62,7 +70,9 @@ public class SessionController { @PostMapping public ResponseEntity create(@RequestBody Session session) { - UUID userId = (UUID) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null); + if (userId == null) return ResponseEntity.status(401).build(); SessionEntity entity = new SessionEntity(); entity.setSessionId(UUID.randomUUID()); entity.setUserId(userId); @@ -76,6 +86,10 @@ public class SessionController { entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0); entity.setLevel(1); sessionRepository.save(entity); + LOGGER.debug("Session gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]", + entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(), + entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(), + entity.getZeitfaktorZeitstrafen()); return ResponseEntity.created( ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSessionId()).toUri() ).build(); @@ -85,6 +99,8 @@ public class SessionController { public ResponseEntity deleteSession(@RequestBody Session session) { return sessionRepository.findById(session.getSessionId()) .map(entity -> { + aktiveSperreRepository.deleteAll(entity.getAktiveSperren()); + mitspielerRepository.deleteAll(entity.getMitspieler()); sessionRepository.delete(entity); return ResponseEntity.accepted().build(); }) @@ -119,7 +135,22 @@ public class SessionController { return ResponseEntity.badRequest().build(); } session.setLetzteAktivitaet(LocalDateTime.now()); - AufgabeAnzeige next = new SessionDurchfuehren(session).getNext(); + SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); + AufgabeAnzeige next = durchfuehren.getNext(); + session.setLevel(durchfuehren.getLevel()); + session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel()); + if (next == null) { + return ResponseEntity.noContent().build(); + } + next.setLevel(durchfuehren.getLevel()); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Neue Aufgabe [sessionId={}, level={}, aufgaben={}/{}, aktiveSperren={}]", + sessionId, session.getLevel(), session.getAufgabenAufAktuellemLevel(), + session.getAufgabenProLevel(), session.getAktiveSperren().size()); + session.getAktiveSperren().forEach(s -> + LOGGER.debug(" Sperre [mitspieler={}, {}min, ende={}]", + s.getMitspieler().getName(), s.getMinuten(), s.getEndzeit())); + } return ResponseEntity.ok(next); } catch (Exception exception) { LOGGER.error(exception.getMessage(), exception); @@ -150,14 +181,48 @@ public class SessionController { return ResponseEntity.accepted().build(); } + @GetMapping("/{sessionId}/finisher") + public ResponseEntity> getFinisher(@PathVariable UUID sessionId) { + try { + SessionEntity session = sessionRepository.findById(sessionId).orElse(null); + if (session == null) return ResponseEntity.badRequest().build(); + SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); + return ResponseEntity.ok(durchfuehren.getFinisher()); + } catch (Exception exception) { + LOGGER.error(exception.getMessage(), exception); + return ResponseEntity.internalServerError().build(); + } + } + + @PostMapping("/{sessionId}/backToLevel5") + public ResponseEntity backToLevel5(@PathVariable UUID sessionId) { + try { + SessionEntity session = sessionRepository.findById(sessionId).orElse(null); + if (session == null) return ResponseEntity.badRequest().build(); + SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); + durchfuehren.backToLvl5(); + session.setLevel(durchfuehren.getLevel()); + session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel()); + sessionRepository.save(session); + return ResponseEntity.accepted().build(); + } catch (Exception exception) { + LOGGER.error(exception.getMessage(), exception); + return ResponseEntity.internalServerError().build(); + } + } + private Session toSession(SessionEntity entity) { Session session = new Session(); session.setSessionId(entity.getSessionId()); session.setUserId(entity.getUserId()); - session.setAufgabenProLevel(entity.getAufgabenAufAktuellemLevel()); + session.setAufgabenProLevel(entity.getAufgabenProLevel()); session.setWahrscheinlichkeitSperre(entity.getWahrscheinlichkeitSperre()); session.setWahrscheinlichkeitStrafe(entity.getWahrscheinlichkeitStrafe()); session.setZeitfaktorZeitstrafen(entity.getZeitfaktorZeitstrafen()); + session.setLevel(entity.getLevel()); + session.setAufgabenAufAktuellemLevel(entity.getAufgabenAufAktuellemLevel()); + session.setStartZeit(entity.getStartZeit()); + session.setLetzteAktivitaet(entity.getLetzteAktivitaet()); return session; } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SperreController.java b/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SperreController.java index 144af24..e77ee10 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SperreController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/controller/SperreController.java @@ -1,6 +1,9 @@ package de.oaa.xxx.session.controller; +import de.oaa.xxx.session.AktiveSperre; +import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.session.entity.AktiveSperreEntity; +import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.session.repository.SessionRepository; @@ -21,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @RestController("sessionSperreController") @RequestMapping("/session/sperre") @@ -70,6 +74,24 @@ public class SperreController { } } + @GetMapping("/aktive") + public ResponseEntity> getAktiveSperren(@RequestParam UUID sessionId) { + try { + SessionEntity session = sessionRepository.findById(sessionId).orElse(null); + if (session == null) return ResponseEntity.noContent().build(); + List mitspielerList = session.getMitspieler().stream() + .map(m -> m.toMitspieler()) + .collect(Collectors.toList()); + List sperren = session.getAktiveSperren().stream() + .map(e -> e.toSperre(mitspielerList)) + .collect(Collectors.toList()); + return ResponseEntity.ok(sperren); + } catch (Exception exception) { + LOGGER.error(exception.getMessage(), exception); + return ResponseEntity.internalServerError().build(); + } + } + @PostMapping("/verlaengern") public ResponseEntity aktiveVerlaengern(@RequestBody SperrenVerlaengernCallback callback) { if (callback == null || callback.getSpielerId() == null || callback.getFaktor() == null) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/AktiveSperreEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/AktiveSperreEntity.java index 2ff0cf5..04db10a 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/AktiveSperreEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/AktiveSperreEntity.java @@ -84,6 +84,12 @@ public class AktiveSperreEntity { return sperre; } + @Override + public String toString() { + return "AktiveSperreEntity[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null) + + ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]"; + } + private Mitspieler getMitspielerFromList(List mitspielerList, UUID id) { Optional first = mitspielerList.stream().filter(m -> m.getId().equals(id)).findFirst(); return first.orElse(null); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/MitspielerEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/MitspielerEntity.java index 6c847b3..4fed745 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/MitspielerEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/MitspielerEntity.java @@ -78,6 +78,12 @@ public class MitspielerEntity { public List getAktiveSperren() { return aktiveSperren; } public void setAktiveSperren(List aktiveSperren) { this.aktiveSperren = aktiveSperren; } + @Override + public String toString() { + return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name + + ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]"; + } + public Mitspieler toMitspieler() { Mitspieler mitspieler = new Mitspieler(); mitspieler.setGeschlecht(geschlecht); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/SessionEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/SessionEntity.java index 6363b41..20ed28f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/entity/SessionEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/entity/SessionEntity.java @@ -82,4 +82,12 @@ public class SessionEntity { public Double getZeitfaktorZeitstrafen() { return zeitfaktorZeitstrafen; } public void setZeitfaktorZeitstrafen(Double zeitfaktorZeitstrafen) { this.zeitfaktorZeitstrafen = zeitfaktorZeitstrafen; } + + @Override + public String toString() { + return "SessionEntity[sessionId=" + sessionId + ", userId=" + userId + + ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel + + ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%" + + ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperreCallback.java b/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperreCallback.java index a840c4d..2633076 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperreCallback.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperreCallback.java @@ -18,4 +18,9 @@ public class SperreCallback extends Callback { public String getReleaseText() { return releaseText; } public void setReleaseText(String releaseText) { this.releaseText = releaseText; } + + @Override + public String toString() { + return "SperreCallback[sessionId=" + getSessionId() + ", sperreId=" + sperreId + ", spielerId=" + spielerId + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperrenVerlaengernCallback.java b/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperrenVerlaengernCallback.java index e7db136..58bf7fa 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperrenVerlaengernCallback.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/session/sperre/SperrenVerlaengernCallback.java @@ -14,4 +14,9 @@ public class SperrenVerlaengernCallback extends Callback { public Integer getFaktor() { return faktor; } public void setFaktor(Integer faktor) { this.faktor = faktor; } + + @Override + public String toString() { + return "SperrenVerlaengernCallback[sessionId=" + getSessionId() + ", spielerId=" + spielerId + ", faktor=" + faktor + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/ProfileImageController.java b/xxxthegame/src/main/java/de/oaa/xxx/social/ProfileImageController.java new file mode 100644 index 0000000..1da6ed9 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/ProfileImageController.java @@ -0,0 +1,114 @@ +package de.oaa.xxx.social; + +import de.oaa.xxx.social.dto.ProfileImageDto; +import de.oaa.xxx.social.entity.ProfileImageEntity; +import de.oaa.xxx.social.entity.ProfileImageLikeEntity; +import de.oaa.xxx.social.repository.ProfileImageLikeRepository; +import de.oaa.xxx.social.repository.ProfileImageRepository; +import de.oaa.xxx.user.UserRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/social/profile-images") +public class ProfileImageController { + + private static final int MAX_IMAGES_PER_USER = 20; + + private final ProfileImageRepository profileImageRepository; + private final ProfileImageLikeRepository profileImageLikeRepository; + private final UserRepository userRepository; + + public ProfileImageController(ProfileImageRepository profileImageRepository, + ProfileImageLikeRepository profileImageLikeRepository, + UserRepository userRepository) { + this.profileImageRepository = profileImageRepository; + this.profileImageLikeRepository = profileImageLikeRepository; + this.userRepository = userRepository; + } + + record UploadRequest(String imageData) {} + + @PostMapping + public ResponseEntity uploadImage(@RequestBody UploadRequest request, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + if (request.imageData() == null || request.imageData().isBlank()) { + return ResponseEntity.badRequest().build(); + } + if (profileImageRepository.countByUserId(myId) >= MAX_IMAGES_PER_USER) { + return ResponseEntity.status(422).build(); + } + + ProfileImageEntity entity = new ProfileImageEntity(); + entity.setImageId(UUID.randomUUID()); + entity.setUserId(myId); + entity.setImageData(request.imageData()); + entity.setUploadedAt(LocalDateTime.now()); + profileImageRepository.save(entity); + + return ResponseEntity.status(201).body(toDto(entity, myId)); + } + + @GetMapping + public ResponseEntity> getImages(@RequestParam UUID userId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List images = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId); + List dtos = images.stream().map(img -> toDto(img, myId)).toList(); + return ResponseEntity.ok(dtos); + } + + @DeleteMapping("/{imageId}") + public ResponseEntity deleteImage(@PathVariable UUID imageId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var imgOpt = profileImageRepository.findById(imageId); + if (imgOpt.isEmpty()) return ResponseEntity.notFound().build(); + if (!imgOpt.get().getUserId().equals(myId)) return ResponseEntity.status(403).build(); + + profileImageLikeRepository.deleteByImageId(imageId); + profileImageRepository.delete(imgOpt.get()); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{imageId}/like") + public ResponseEntity toggleLike(@PathVariable UUID imageId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + if (profileImageRepository.findById(imageId).isEmpty()) return ResponseEntity.notFound().build(); + + var existing = profileImageLikeRepository.findByImageIdAndUserId(imageId, myId); + if (existing.isPresent()) { + profileImageLikeRepository.delete(existing.get()); + } else { + ProfileImageLikeEntity like = new ProfileImageLikeEntity(); + like.setLikeId(UUID.randomUUID()); + like.setImageId(imageId); + like.setUserId(myId); + like.setLikedAt(LocalDateTime.now()); + profileImageLikeRepository.save(like); + } + return ResponseEntity.ok().build(); + } + + private ProfileImageDto toDto(ProfileImageEntity entity, UUID myId) { + long likeCount = profileImageLikeRepository.countByImageId(entity.getImageId()); + boolean likedByMe = profileImageLikeRepository.findByImageIdAndUserId(entity.getImageId(), myId).isPresent(); + return new ProfileImageDto(entity.getImageId(), entity.getUserId(), entity.getImageData(), + entity.getUploadedAt(), likeCount, likedByMe); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java b/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java new file mode 100644 index 0000000..b60f4c1 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java @@ -0,0 +1,273 @@ +package de.oaa.xxx.social; + +import de.oaa.xxx.social.dto.ConversationSummary; +import de.oaa.xxx.social.dto.FriendshipDto; +import de.oaa.xxx.social.dto.MessageDto; +import de.oaa.xxx.social.dto.UserProfile; +import de.oaa.xxx.social.entity.FriendshipEntity; +import de.oaa.xxx.social.entity.FriendshipEntity.Status; +import de.oaa.xxx.social.entity.MessageEntity; +import de.oaa.xxx.social.repository.FriendshipRepository; +import de.oaa.xxx.social.repository.MessageRepository; +import de.oaa.xxx.user.UserEntity; +import de.oaa.xxx.user.UserRepository; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.LocalDateTime; +import java.util.*; + +@RestController +@RequestMapping("/social") +public class SocialController { + + private final UserRepository userRepository; + private final FriendshipRepository friendshipRepository; + private final MessageRepository messageRepository; + + public SocialController(UserRepository userRepository, + FriendshipRepository friendshipRepository, + MessageRepository messageRepository) { + this.userRepository = userRepository; + this.friendshipRepository = friendshipRepository; + this.messageRepository = messageRepository; + } + + record FriendRequestBody(UUID receiverId) {} + record FriendshipActionBody(UUID friendshipId) {} + record SendMessageBody(UUID receiverId, String text) {} + + // ── User Profile ── + + @GetMapping("/users/{userId}") + public ResponseEntity getUserProfile(@PathVariable UUID userId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + return userRepository.findById(userId) + .map(u -> ResponseEntity.ok(toUserProfileWithStatus(u, myId))) + .orElse(ResponseEntity.notFound().build()); + } + + // ── User Search ── + + @GetMapping("/users/search") + public ResponseEntity> searchUsers(@RequestParam String q, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List results = userRepository.findByNameContainingIgnoreCase(q); + List profiles = results.stream() + .filter(u -> !u.getUserId().equals(myId)) + .limit(20) + .map(u -> toUserProfileWithStatus(u, myId)) + .toList(); + return ResponseEntity.ok(profiles); + } + + // ── Friendship ── + + @PostMapping("/friends/request") + public ResponseEntity sendFriendRequest(@RequestBody FriendRequestBody body, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + if (friendshipRepository.findExisting(myId, body.receiverId()).isPresent()) { + return ResponseEntity.status(409).build(); + } + FriendshipEntity f = new FriendshipEntity(); + f.setFriendshipId(UUID.randomUUID()); + f.setSenderId(myId); + f.setReceiverId(body.receiverId()); + f.setStatus(Status.PENDING); + f.setCreatedAt(LocalDateTime.now()); + friendshipRepository.save(f); + return ResponseEntity.status(201).build(); + } + + @PostMapping("/friends/accept") + public ResponseEntity acceptFriendRequest(@RequestBody FriendshipActionBody body, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var fOpt = friendshipRepository.findById(body.friendshipId()); + if (fOpt.isEmpty()) return ResponseEntity.notFound().build(); + FriendshipEntity f = fOpt.get(); + if (!f.getReceiverId().equals(myId)) return ResponseEntity.status(403).build(); + + f.setStatus(Status.ACCEPTED); + friendshipRepository.save(f); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/friends/reject") + public ResponseEntity rejectOrRemoveFriend(@RequestBody FriendshipActionBody body, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var fOpt = friendshipRepository.findById(body.friendshipId()); + if (fOpt.isEmpty()) return ResponseEntity.notFound().build(); + FriendshipEntity f = fOpt.get(); + if (!f.getSenderId().equals(myId) && !f.getReceiverId().equals(myId)) { + return ResponseEntity.status(403).build(); + } + friendshipRepository.delete(f); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/friends") + public ResponseEntity> getFriends(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List dtos = friendshipRepository.findFriends(myId, Status.ACCEPTED).stream() + .map(f -> { + UUID friendId = f.getSenderId().equals(myId) ? f.getReceiverId() : f.getSenderId(); + return userRepository.findById(friendId) + .map(u -> new FriendshipDto( + f.getFriendshipId(), + new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "FRIEND"), + f.getStatus().name(), + f.getCreatedAt())) + .orElse(null); + }) + .filter(Objects::nonNull) + .toList(); + return ResponseEntity.ok(dtos); + } + + @GetMapping("/friends/pending") + public ResponseEntity> getPendingRequests(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List dtos = friendshipRepository.findByReceiverIdAndStatus(myId, Status.PENDING).stream() + .map(f -> userRepository.findById(f.getSenderId()) + .map(u -> new FriendshipDto( + f.getFriendshipId(), + new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "PENDING_RECEIVED"), + f.getStatus().name(), + f.getCreatedAt())) + .orElse(null)) + .filter(Objects::nonNull) + .toList(); + return ResponseEntity.ok(dtos); + } + + @GetMapping("/friends/pending/count") + public ResponseEntity getPendingCount(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + return ResponseEntity.ok(friendshipRepository.countByReceiverIdAndStatus(myId, Status.PENDING)); + } + + // ── Messages ── + + @PostMapping("/messages") + public ResponseEntity sendMessage(@RequestBody SendMessageBody body, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build(); + + MessageEntity msg = new MessageEntity(); + msg.setMessageId(UUID.randomUUID()); + msg.setSenderId(myId); + msg.setReceiverId(body.receiverId()); + msg.setText(body.text().trim()); + msg.setSentAt(LocalDateTime.now()); + messageRepository.save(msg); + return ResponseEntity.status(201).build(); + } + + @GetMapping("/messages") + public ResponseEntity> getConversations(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List allMessages = messageRepository.findAllByUser(myId); + + // Group by partner, keep most recent message per partner + Map latestByPartner = new LinkedHashMap<>(); + for (MessageEntity m : allMessages) { + UUID partnerId = m.getSenderId().equals(myId) ? m.getReceiverId() : m.getSenderId(); + latestByPartner.putIfAbsent(partnerId, m); + } + + List summaries = new ArrayList<>(); + for (Map.Entry entry : latestByPartner.entrySet()) { + UUID partnerId = entry.getKey(); + MessageEntity lastMsg = entry.getValue(); + var partnerOpt = userRepository.findById(partnerId); + if (partnerOpt.isEmpty()) continue; + + UserProfile partnerProfile = toUserProfileWithStatus(partnerOpt.get(), myId); + MessageDto lastMsgDto = toMessageDto(lastMsg); + long unreadCount = allMessages.stream() + .filter(m -> m.getSenderId().equals(partnerId) + && m.getReceiverId().equals(myId) + && m.getReadAt() == null) + .count(); + summaries.add(new ConversationSummary(partnerProfile, lastMsgDto, unreadCount)); + } + return ResponseEntity.ok(summaries); + } + + @GetMapping("/messages/unread/count") + public ResponseEntity getUnreadCount(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + return ResponseEntity.ok(messageRepository.countUnread(myId)); + } + + @GetMapping("/messages/{partnerId}") + public ResponseEntity> getConversation(@PathVariable UUID partnerId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List messages = messageRepository.findConversation(myId, partnerId, PageRequest.of(0, 50)); + messageRepository.markAsRead(myId, partnerId, LocalDateTime.now()); + + return ResponseEntity.ok(messages.stream().map(this::toMessageDto).toList()); + } + + // ── Helpers ── + + private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) { + String status = "NONE"; + var existing = friendshipRepository.findExisting(myId, user.getUserId()); + if (existing.isPresent()) { + FriendshipEntity f = existing.get(); + if (f.getStatus() == Status.ACCEPTED) { + status = "FRIEND"; + } else if (f.getSenderId().equals(myId)) { + status = "PENDING_SENT"; + } else { + status = "PENDING_RECEIVED"; + } + } + return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(), status); + } + + private MessageDto toMessageDto(MessageEntity m) { + String senderName = userRepository.findById(m.getSenderId()) + .map(UserEntity::getName) + .orElse("Unbekannt"); + return new MessageDto( + m.getMessageId(), m.getSenderId(), senderName, + m.getReceiverId(), m.getText(), m.getSentAt(), m.getReadAt() != null); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ConversationSummary.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ConversationSummary.java new file mode 100644 index 0000000..c15f880 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ConversationSummary.java @@ -0,0 +1,3 @@ +package de.oaa.xxx.social.dto; + +public record ConversationSummary(UserProfile partner, MessageDto lastMessage, long unreadCount) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/FriendshipDto.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/FriendshipDto.java new file mode 100644 index 0000000..3534c48 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/FriendshipDto.java @@ -0,0 +1,6 @@ +package de.oaa.xxx.social.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record FriendshipDto(UUID friendshipId, UserProfile user, String status, LocalDateTime createdAt) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/MessageDto.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/MessageDto.java new file mode 100644 index 0000000..74a6ae6 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/MessageDto.java @@ -0,0 +1,6 @@ +package de.oaa.xxx.social.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record MessageDto(UUID messageId, UUID senderId, String senderName, UUID receiverId, String text, LocalDateTime sentAt, boolean read) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ProfileImageDto.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ProfileImageDto.java new file mode 100644 index 0000000..776661d --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/ProfileImageDto.java @@ -0,0 +1,7 @@ +package de.oaa.xxx.social.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record ProfileImageDto(UUID imageId, UUID userId, String imageData, + LocalDateTime uploadedAt, long likeCount, boolean likedByMe) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java new file mode 100644 index 0000000..2297277 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java @@ -0,0 +1,5 @@ +package de.oaa.xxx.social.dto; + +import java.util.UUID; + +public record UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/entity/FriendshipEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/FriendshipEntity.java new file mode 100644 index 0000000..5f4c959 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/FriendshipEntity.java @@ -0,0 +1,44 @@ +package de.oaa.xxx.social.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "friendship") +public class FriendshipEntity { + + public enum Status { PENDING, ACCEPTED } + + @Id + @Column + private UUID friendshipId; + + @Column(nullable = false) + private UUID senderId; + + @Column(nullable = false) + private UUID receiverId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private Status status; + + @Column(nullable = false) + private LocalDateTime createdAt; + + public UUID getFriendshipId() { return friendshipId; } + public void setFriendshipId(UUID friendshipId) { this.friendshipId = friendshipId; } + + public UUID getSenderId() { return senderId; } + public void setSenderId(UUID senderId) { this.senderId = senderId; } + + public UUID getReceiverId() { return receiverId; } + public void setReceiverId(UUID receiverId) { this.receiverId = receiverId; } + + public Status getStatus() { return status; } + public void setStatus(Status status) { this.status = status; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/entity/MessageEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/MessageEntity.java new file mode 100644 index 0000000..7aac334 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/MessageEntity.java @@ -0,0 +1,47 @@ +package de.oaa.xxx.social.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "message") +public class MessageEntity { + + @Id + @Column + private UUID messageId; + + @Column(nullable = false) + private UUID senderId; + + @Column(nullable = false) + private UUID receiverId; + + @Column(columnDefinition = "MEDIUMTEXT", nullable = false) + private String text; + + @Column(nullable = false) + private LocalDateTime sentAt; + + @Column + private LocalDateTime readAt; + + public UUID getMessageId() { return messageId; } + public void setMessageId(UUID messageId) { this.messageId = messageId; } + + public UUID getSenderId() { return senderId; } + public void setSenderId(UUID senderId) { this.senderId = senderId; } + + public UUID getReceiverId() { return receiverId; } + public void setReceiverId(UUID receiverId) { this.receiverId = receiverId; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public LocalDateTime getSentAt() { return sentAt; } + public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; } + + public LocalDateTime getReadAt() { return readAt; } + public void setReadAt(LocalDateTime readAt) { this.readAt = readAt; } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageEntity.java new file mode 100644 index 0000000..1ee3ddb --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageEntity.java @@ -0,0 +1,35 @@ +package de.oaa.xxx.social.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "profile_image") +public class ProfileImageEntity { + + @Id + @Column + private UUID imageId; + + @Column(nullable = false) + private UUID userId; + + @Column(columnDefinition = "MEDIUMTEXT", nullable = false) + private String imageData; + + @Column(nullable = false) + private LocalDateTime uploadedAt; + + public UUID getImageId() { return imageId; } + public void setImageId(UUID imageId) { this.imageId = imageId; } + + public UUID getUserId() { return userId; } + public void setUserId(UUID userId) { this.userId = userId; } + + public String getImageData() { return imageData; } + public void setImageData(String imageData) { this.imageData = imageData; } + + public LocalDateTime getUploadedAt() { return uploadedAt; } + public void setUploadedAt(LocalDateTime uploadedAt) { this.uploadedAt = uploadedAt; } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageLikeEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageLikeEntity.java new file mode 100644 index 0000000..dbb526f --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/entity/ProfileImageLikeEntity.java @@ -0,0 +1,35 @@ +package de.oaa.xxx.social.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "profile_image_like") +public class ProfileImageLikeEntity { + + @Id + @Column + private UUID likeId; + + @Column(nullable = false) + private UUID imageId; + + @Column(nullable = false) + private UUID userId; + + @Column(nullable = false) + private LocalDateTime likedAt; + + public UUID getLikeId() { return likeId; } + public void setLikeId(UUID likeId) { this.likeId = likeId; } + + public UUID getImageId() { return imageId; } + public void setImageId(UUID imageId) { this.imageId = imageId; } + + public UUID getUserId() { return userId; } + public void setUserId(UUID userId) { this.userId = userId; } + + public LocalDateTime getLikedAt() { return likedAt; } + public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/repository/FriendshipRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/FriendshipRepository.java new file mode 100644 index 0000000..1fc4375 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/FriendshipRepository.java @@ -0,0 +1,24 @@ +package de.oaa.xxx.social.repository; + +import de.oaa.xxx.social.entity.FriendshipEntity; +import de.oaa.xxx.social.entity.FriendshipEntity.Status; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface FriendshipRepository extends JpaRepository { + + List findByReceiverIdAndStatus(UUID receiverId, Status status); + + long countByReceiverIdAndStatus(UUID receiverId, Status status); + + @Query("SELECT f FROM FriendshipEntity f WHERE (f.senderId = :userId OR f.receiverId = :userId) AND f.status = :status") + List findFriends(@Param("userId") UUID userId, @Param("status") Status status); + + @Query("SELECT f FROM FriendshipEntity f WHERE (f.senderId = :userA AND f.receiverId = :userB) OR (f.senderId = :userB AND f.receiverId = :userA)") + Optional findExisting(@Param("userA") UUID userA, @Param("userB") UUID userB); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/repository/MessageRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/MessageRepository.java new file mode 100644 index 0000000..07979cc --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/MessageRepository.java @@ -0,0 +1,30 @@ +package de.oaa.xxx.social.repository; + +import de.oaa.xxx.social.entity.MessageEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public interface MessageRepository extends JpaRepository { + + @Query("SELECT m FROM MessageEntity m WHERE (m.senderId = :userA AND m.receiverId = :userB) OR (m.senderId = :userB AND m.receiverId = :userA) ORDER BY m.sentAt DESC") + List findConversation(@Param("userA") UUID userA, @Param("userB") UUID userB, Pageable pageable); + + @Query("SELECT m FROM MessageEntity m WHERE m.senderId = :userId OR m.receiverId = :userId ORDER BY m.sentAt DESC") + List findAllByUser(@Param("userId") UUID userId); + + @Query("SELECT COUNT(m) FROM MessageEntity m WHERE m.receiverId = :userId AND m.readAt IS NULL") + long countUnread(@Param("userId") UUID userId); + + @Modifying + @Transactional + @Query("UPDATE MessageEntity m SET m.readAt = :now WHERE m.senderId = :partnerId AND m.receiverId = :userId AND m.readAt IS NULL") + void markAsRead(@Param("userId") UUID userId, @Param("partnerId") UUID partnerId, @Param("now") LocalDateTime now); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageLikeRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageLikeRepository.java new file mode 100644 index 0000000..2e719e0 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageLikeRepository.java @@ -0,0 +1,28 @@ +package de.oaa.xxx.social.repository; + +import de.oaa.xxx.social.entity.ProfileImageLikeEntity; +import jakarta.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; +import java.util.UUID; + +public interface ProfileImageLikeRepository extends JpaRepository { + + Optional findByImageIdAndUserId(UUID imageId, UUID userId); + + long countByImageId(UUID imageId); + + @Modifying + @Transactional + @Query("DELETE FROM ProfileImageLikeEntity l WHERE l.imageId = :imageId") + void deleteByImageId(@Param("imageId") UUID imageId); + + @Modifying + @Transactional + @Query("DELETE FROM ProfileImageLikeEntity l WHERE l.userId = :userId") + void deleteByUserId(@Param("userId") UUID userId); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageRepository.java new file mode 100644 index 0000000..fe4a94a --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/repository/ProfileImageRepository.java @@ -0,0 +1,14 @@ +package de.oaa.xxx.social.repository; + +import de.oaa.xxx.social.entity.ProfileImageEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface ProfileImageRepository extends JpaRepository { + + List findByUserIdOrderByUploadedAtDesc(UUID userId); + + long countByUserId(UUID userId); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java b/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java index 11b4f87..9c7f4a3 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java @@ -62,6 +62,18 @@ public class LoginController { .orElse(ResponseEntity.status(401).build()); } + @GetMapping("/logout") + public void logout(HttpServletResponse response) throws java.io.IOException { + ResponseCookie cookie = ResponseCookie.from("jwt", "") + .httpOnly(true) + .sameSite("Strict") + .path("/") + .maxAge(0) + .build(); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + response.sendRedirect("/"); + } + @GetMapping("/{userId}") public ResponseEntity get(@PathVariable UUID userId) { return userRepository.findById(userId) diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/User.java b/xxxthegame/src/main/java/de/oaa/xxx/user/User.java index 397f2d4..f8668a2 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/User.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/User.java @@ -8,6 +8,7 @@ public class User { private String name; private String email; private String password; + private String profilePicture; public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } @@ -20,4 +21,12 @@ public class User { public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } + + public String getProfilePicture() { return profilePicture; } + public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; } + + @Override + public String toString() { + return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]"; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java index 9aad1ad..ce00baa 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java @@ -1,14 +1,39 @@ package de.oaa.xxx.user; +import java.security.Principal; +import java.util.List; +import java.util.UUID; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.UUID; +import de.oaa.xxx.aufgaben.repository.AufgabeRepository; +import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository; +import de.oaa.xxx.aufgaben.repository.FavoritRepository; +import de.oaa.xxx.aufgaben.repository.GruppenAboRepository; +import de.oaa.xxx.aufgaben.repository.SperreRepository; +import de.oaa.xxx.aufgaben.repository.StrafeRepository; +import de.oaa.xxx.aufgaben.repository.ToyRepository; +import de.oaa.xxx.emailchange.EmailChangeRepository; +import de.oaa.xxx.passwordreset.PasswordResetRepository; +import de.oaa.xxx.registration.RegistrationRepository; +import de.oaa.xxx.session.entity.AktiveSperreEntity; +import de.oaa.xxx.session.entity.MitspielerEntity; +import de.oaa.xxx.session.repository.AktiveSperreRepository; +import de.oaa.xxx.session.repository.MitspielerRepository; +import de.oaa.xxx.session.repository.SessionRepository; +import de.oaa.xxx.social.repository.ProfileImageLikeRepository; +import de.oaa.xxx.social.repository.ProfileImageRepository; +import jakarta.transaction.Transactional; @RestController @RequestMapping("/user") @@ -17,9 +42,150 @@ public class UserController { private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); private final UserRepository userRepository; + private final RegistrationRepository registrationRepository; + private final AufgabenGruppeRepository aufgabenGruppeRepository; + private final AufgabeRepository aufgabeRepository; + private final StrafeRepository strafeRepository; + private final SperreRepository sperreRepository; + private final ToyRepository toyRepository; + private final FavoritRepository favoritRepository; + private final GruppenAboRepository gruppenAboRepository; + private final SessionRepository sessionRepository; + private final AktiveSperreRepository aktiveSperreRepository; + private final MitspielerRepository mitspielerRepository; + private final EmailChangeRepository emailChangeRepository; + private final PasswordResetRepository passwordResetRepository; + private final ProfileImageRepository profileImageRepository; + private final ProfileImageLikeRepository profileImageLikeRepository; - public UserController(UserRepository userRepository) { + public UserController(UserRepository userRepository, + RegistrationRepository registrationRepository, + AufgabenGruppeRepository aufgabenGruppeRepository, + AufgabeRepository aufgabeRepository, + StrafeRepository strafeRepository, + SperreRepository sperreRepository, + ToyRepository toyRepository, + FavoritRepository favoritRepository, + GruppenAboRepository gruppenAboRepository, + SessionRepository sessionRepository, + AktiveSperreRepository aktiveSperreRepository, + MitspielerRepository mitspielerRepository, + EmailChangeRepository emailChangeRepository, + PasswordResetRepository passwordResetRepository, + ProfileImageRepository profileImageRepository, + ProfileImageLikeRepository profileImageLikeRepository) { this.userRepository = userRepository; + this.registrationRepository = registrationRepository; + this.aufgabenGruppeRepository = aufgabenGruppeRepository; + this.aufgabeRepository = aufgabeRepository; + this.strafeRepository = strafeRepository; + this.sperreRepository = sperreRepository; + this.toyRepository = toyRepository; + this.favoritRepository = favoritRepository; + this.gruppenAboRepository = gruppenAboRepository; + this.sessionRepository = sessionRepository; + this.aktiveSperreRepository = aktiveSperreRepository; + this.mitspielerRepository = mitspielerRepository; + this.emailChangeRepository = emailChangeRepository; + this.passwordResetRepository = passwordResetRepository; + this.profileImageRepository = profileImageRepository; + this.profileImageLikeRepository = profileImageLikeRepository; + } + + record ProfilePictureRequest(String picture, String pictureHq) {} + record NameChangeRequest(String name) {} + + @PutMapping("/me/picture") + public ResponseEntity updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) { + var user = userRepository.findByEmail(principal.getName()); + if (user.isEmpty()) return ResponseEntity.status(401).build(); + user.get().setProfilePicture(request.picture()); + user.get().setProfilePictureHq(request.pictureHq()); + userRepository.save(user.get()); + return ResponseEntity.ok().build(); + } + + @PutMapping("/me/name") + public ResponseEntity updateName(@RequestBody NameChangeRequest request, Principal principal) { + String newName = request.name(); + if (userRepository.findByName(newName).isPresent() + || registrationRepository.findByName(newName).isPresent()) { + return ResponseEntity.status(409).build(); + } + var user = userRepository.findByEmail(principal.getName()); + if (user.isEmpty()) return ResponseEntity.status(401).build(); + user.get().setName(newName); + userRepository.save(user.get()); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/me") + @Transactional + public ResponseEntity deleteAccount(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + var user = userOpt.get(); + UUID userId = user.getUserId(); + String email = user.getEmail(); + + LOGGER.info("Lösche Konto für User {}", email); + + // 1. Delete user's AufgabenGruppen and all their content + var gruppen = aufgabenGruppeRepository.findByUserId(userId); + if (!gruppen.isEmpty()) { + aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen)); + strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen)); + sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen)); + for (var gruppe : gruppen) { + gruppenAboRepository.deleteByAufgabenGruppe(gruppe); + favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId()); + } + aufgabenGruppeRepository.deleteAll(gruppen); + } + + // 2. Delete user's Toys (join table refs already cleared above) + toyRepository.deleteAll(toyRepository.findByUserId(userId)); + + // 3. Delete user's own Favoriten and Gruppenabos (to other groups) + favoritRepository.deleteAll(favoritRepository.findByUserId(userId)); + gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId)); + + // 4. Delete Session with Mitspieler and AktiveSperre + var sessionOpt = sessionRepository.findByUserId(userId); + if (sessionOpt.isPresent()) { + var session = sessionOpt.get(); + List sperren = session.getAktiveSperren(); + List mitspieler = session.getMitspieler(); + aktiveSperreRepository.deleteAll(sperren); + mitspielerRepository.deleteAll(mitspieler); + sessionRepository.delete(session); + } + + // 5. Delete pending tokens + emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete); + passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete); + + // 5b. Delete profile images and likes + var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId); + for (var img : profileImages) { + profileImageLikeRepository.deleteByImageId(img.getImageId()); + } + profileImageRepository.deleteAll(profileImages); + profileImageLikeRepository.deleteByUserId(userId); + + // 6. Delete user + userRepository.delete(user); + + // Clear JWT cookie + ResponseCookie cookie = ResponseCookie.from("jwt", "") + .httpOnly(true) + .sameSite("Strict") + .path("/") + .maxAge(0) + .build(); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .build(); } @PostMapping diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java index 52c0963..21dfb01 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java @@ -21,6 +21,12 @@ public class UserEntity { @Column private String password; + @Column(columnDefinition = "TEXT") + private String profilePicture; + + @Column(columnDefinition = "MEDIUMTEXT") + private String profilePictureHq; + public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } @@ -33,11 +39,23 @@ public class UserEntity { public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } + public String getProfilePicture() { return profilePicture; } + public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; } + + public String getProfilePictureHq() { return profilePictureHq; } + public void setProfilePictureHq(String profilePictureHq) { this.profilePictureHq = profilePictureHq; } + + @Override + public String toString() { + return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]"; + } + public User toUser() { User user = new User(); user.setEmail(email); user.setName(name); user.setUserId(userId); + user.setProfilePicture(profilePicture); return user; } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java index b646539..8d01c4b 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java @@ -2,12 +2,14 @@ package de.oaa.xxx.user; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { Optional findByEmailAndPassword(String email, String password); - Optional findByEmail(String email); + Optional findByName(String name); + List findByNameContainingIgnoreCase(String name); } diff --git a/xxxthegame/src/main/resources/application.properties b/xxxthegame/src/main/resources/application.properties index 721341d..41929d8 100644 --- a/xxxthegame/src/main/resources/application.properties +++ b/xxxthegame/src/main/resources/application.properties @@ -43,6 +43,9 @@ app.theme.color-text=#eeeeee app.theme.color-muted=#888888 app.theme.color-success=#2ecc71 +# Logging +logging.level.de.oaa.xxx=DEBUG + # Server server.port=8080 server.servlet.context-path=/ diff --git a/xxxthegame/src/main/resources/static/aufgaben.html b/xxxthegame/src/main/resources/static/aufgaben.html index 9ccd053..5b46a98 100644 --- a/xxxthegame/src/main/resources/static/aufgaben.html +++ b/xxxthegame/src/main/resources/static/aufgaben.html @@ -7,70 +7,6 @@ - - -
+ + +
+ +
+ + + + +
+
+
+ +
+ + + + + +
+
+
@@ -513,6 +507,11 @@
+
- - -
- - -
-
- -

Aufgaben

-
- -
+
+
@@ -621,7 +583,6 @@
-
@@ -708,13 +669,15 @@ } list.innerHTML = gruppen.map(g => { _gruppeData[g.gruppenId] = g; - const aufgabenCount = (g.aufgaben || []).length; - const strafeCount = (g.strafen || []).length; - const sperreCount = (g.sperren || []).length; + const aufgabenCount = (g.aufgaben || []).length; + const strafeCount = (g.strafen || []).length; + const sperreCount = (g.sperren || []).length; + const finisherCount = (g.finisher || []).length; const counts = [ - aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '', - strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '', - sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : '' + aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '', + strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '', + sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : '', + finisherCount ? `${finisherCount} Finisher` : '' ].filter(Boolean).join(' · '); const badges = []; @@ -738,9 +701,10 @@
`; }).join(''); @@ -864,6 +828,33 @@ `; } + const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' }; + + function renderFinisher(f, type, gruppenId) { + _itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId }; + const badges = []; + if (f.geschlecht) badges.push(`${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}`); + + const detailRows = []; + if (f.text) detailRows.push(`
${esc(f.text)}
`); + if (f.benoetigtAktiv && f.benoetigtAktiv.length) detailRows.push(`
Aktiv:${werkzeugChips(f.benoetigtAktiv)}
`); + if (f.benoetigtPassiv && f.benoetigtPassiv.length) detailRows.push(`
Passiv:${werkzeugChips(f.benoetigtPassiv)}
`); + if (f.benoetigteToys && f.benoetigteToys.length) detailRows.push(`
Toys:${toyChips(f.benoetigteToys)}
`); + const actionBtns = type === 'user' ? ` +
+ + +
` : ''; + + return `
+
+ ${esc(f.kurzText)} + ${badges.length ? `${badges.join('')}` : ''} +
+ ${(detailRows.length || actionBtns) ? `
${detailRows.join('')}${actionBtns}
` : ''} +
`; + } + // ── Item toggle (detail panel) ── let openItemId = null; @@ -883,8 +874,8 @@ } // ── Item löschen ── - const ITEM_DELETE_URL = { aufgabe: '/aufgabe', strafe: '/strafe', zeitstrafe: '/sperre' }; - const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId' }; + const ITEM_DELETE_URL = { aufgabe: '/aufgabe', strafe: '/strafe', zeitstrafe: '/sperre', finisher: '/finisher' }; + const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' }; function deleteItem(kind, itemId, gruppenId, event) { event.stopPropagation(); @@ -1225,29 +1216,34 @@ if (chip) chip.classList.remove('selected'); } - // Toy-Suchmodal - const toySearchModal = document.getElementById('toySearchModal'); - - document.getElementById('iToyAddBtn').addEventListener('click', openToySearch); - document.getElementById('toySearchDoneBtn').addEventListener('click', closeToySearch); - toySearchModal.addEventListener('click', e => { if (e.target === toySearchModal) closeToySearch(); }); + // Toy-Suche (inline) + document.getElementById('iToyAddBtn').addEventListener('click', toggleToySearch); document.getElementById('toySearchInput').addEventListener('input', renderToySearchResults); - function openToySearch() { - document.getElementById('toySearchInput').value = ''; - document.getElementById('toySearchResults').innerHTML = ''; - document.getElementById('toySearchEmpty').style.display = 'none'; - toySearchModal.classList.add('open'); - _loadAvailableToys() - .then(() => renderToySearchResults()) - .catch(() => { - document.getElementById('toySearchResults').innerHTML = - 'Fehler beim Laden.'; - }); - document.getElementById('toySearchInput').focus(); + function toggleToySearch() { + const area = document.getElementById('iToySearchArea'); + if (area.style.display === 'none') { + area.style.display = 'block'; + document.getElementById('toySearchInput').value = ''; + document.getElementById('toySearchResults').innerHTML = ''; + document.getElementById('toySearchEmpty').style.display = 'none'; + document.getElementById('iToyAddBtn').textContent = '▲ Suche schließen'; + _loadAvailableToys() + .then(() => renderToySearchResults()) + .catch(() => { + document.getElementById('toySearchResults').innerHTML = + 'Fehler beim Laden.'; + }); + document.getElementById('toySearchInput').focus(); + } else { + closeToySearch(); + } } - function closeToySearch() { toySearchModal.classList.remove('open'); } + function closeToySearch() { + document.getElementById('iToySearchArea').style.display = 'none'; + document.getElementById('iToyAddBtn').textContent = '+ Toy hinzufügen'; + } function renderToySearchResults() { const query = document.getElementById('toySearchInput').value.trim().toLowerCase(); @@ -1297,17 +1293,29 @@ let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe' let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items - const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen' }; - const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten' }; + const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' }; + const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' }; function _setupItemModal(kind) { - const isZeit = kind === 'zeitstrafe'; - document.getElementById('iLevelRow').style.display = isZeit ? 'none' : 'block'; - document.getElementById('iWerkzeugAktivRow').style.display = isZeit ? 'none' : 'block'; - document.getElementById('iWerkzeugPassivRow').style.display = isZeit ? 'none' : 'block'; - document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none'; - document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none'; - document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none'; + const isZeit = kind === 'zeitstrafe'; + const isFinisher = kind === 'finisher'; + document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML = + isFinisher + ? 'In Texten können Platzhalter verwendet werden:
' + + '{AKTIV} – Name der Person die kommt
' + + '{PASSIV} – Name der Person die zum Kommen bringt' + : 'In Texten können Platzhalter verwendet werden:
' + + '{AKTIV} – Name des aktiven Parts
' + + '{PASSIV} – Name des passiven Parts'; + document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none'; + document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none'; + document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none'; + document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none'; + document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none'; + document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none'; + document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none'; + document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none'; + document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none'; } function _resetItemFields() { @@ -1319,9 +1327,12 @@ document.getElementById('iMinVon').value = ''; document.getElementById('iMinBis').value = ''; document.getElementById('iReleaseText').value = ''; - document.querySelectorAll('#iWerkzeugAktiv input').forEach(cb => cb.checked = false); - document.querySelectorAll('#iWerkzeugPassiv input').forEach(cb => cb.checked = false); - document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iWerkzeugAktiv input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iWerkzeugPassiv input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iWerkzeugFinisherAktiv input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false); + document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false); _selectedToys = []; renderSelectedToys(); document.getElementById('itemModalError').style.display = 'none'; @@ -1359,6 +1370,13 @@ document.getElementById('iSekBis').value = d.sekundenBis != null ? d.sekundenBis : ''; (d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugAktiv input[value="${w}"]`); if (cb) cb.checked = true; }); (d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugPassiv input[value="${w}"]`); if (cb) cb.checked = true; }); + } else if (d._kind === 'finisher') { + (d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherAktiv input[value="${w}"]`); if (cb) cb.checked = true; }); + (d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherPassiv input[value="${w}"]`); if (cb) cb.checked = true; }); + if (d.geschlecht) { + const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`); + if (rb) rb.checked = true; + } } else { document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : ''; document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : ''; @@ -1372,7 +1390,11 @@ document.getElementById('iKurzText').focus(); } - function closeItemModal() { itemModal.classList.remove('open'); } + function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; } + function togglePlaceholderHint() { + const el = document.getElementById('iPlaceholderHint'); + el.style.display = el.style.display === 'none' ? 'block' : 'none'; + } document.getElementById('itemCancelBtn').addEventListener('click', closeItemModal); itemModal.addEventListener('click', e => { if (e.target === itemModal) closeItemModal(); }); @@ -1408,6 +1430,19 @@ url = isEdit ? `${base}/${currentItemEditId}` : base; method = isEdit ? 'PUT' : 'POST'; + } else if (kind === 'finisher') { + const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value; + if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; } + payload = { + kurzText, text, geschlecht, + gruppeId: isEdit ? undefined : currentItemGruppeId, + benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'), + benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'), + benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) + }; + url = isEdit ? `/finisher/${currentItemEditId}` : '/finisher'; + method = isEdit ? 'PUT' : 'POST'; + } else { const minVon = document.getElementById('iMinVon').value.trim(); if (!minVon) { showItemError('Bitte eine Mindestdauer in Minuten angeben.'); return; } @@ -1422,7 +1457,7 @@ minutenBis: minBis ? parseInt(minBis, 10) : null, releaseText: document.getElementById('iReleaseText').value.trim() || null, sperreFuer, - benoetigteToys: Array.from(_selectedToyIds).map(id => ({ toyId: id })) + benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) }; url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; method = isEdit ? 'PUT' : 'POST'; @@ -1558,27 +1593,12 @@ // ── ESC schließt alle Modals ── document.addEventListener('keydown', e => { if (e.key !== 'Escape') return; - if (toySearchModal.classList.contains('open')) { closeToySearch(); return; } if (publishModal.classList.contains('open')) { closePublishModal(); return; } if (gruppeModal.classList.contains('open')) { closeGruppeModal(); return; } if (itemModal.classList.contains('open')) { closeItemModal(); return; } }); - // ── Burger menu ── - const sidebar = document.getElementById('sidebar'); - const burgerBtn = document.getElementById('burgerBtn'); - const overlay = document.getElementById('overlay'); - function openMenu() { - sidebar.classList.add('open'); overlay.classList.add('visible'); - burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen'); - } - function closeMenu() { - sidebar.classList.remove('open'); overlay.classList.remove('visible'); - burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen'); - } - burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu()); - overlay.addEventListener('click', closeMenu); - sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); })); + diff --git a/xxxthegame/src/main/resources/static/benutzer.html b/xxxthegame/src/main/resources/static/benutzer.html new file mode 100644 index 0000000..f466a7e --- /dev/null +++ b/xxxthegame/src/main/resources/static/benutzer.html @@ -0,0 +1,443 @@ + + + + + + Profil – XXX The Game + + + + + + +
+
+

Wird geladen…

+ + +
+
+ + + + + + + diff --git a/xxxthegame/src/main/resources/static/css/style.css b/xxxthegame/src/main/resources/static/css/style.css index 37cd0b6..fdeccf7 100644 --- a/xxxthegame/src/main/resources/static/css/style.css +++ b/xxxthegame/src/main/resources/static/css/style.css @@ -132,16 +132,278 @@ button.secondary:hover { .message.error { background: #3d0f1a; - border: 1px solid var(--color-primary); + border: 2px solid var(--color-primary); color: var(--color-primary); } +.message.warning { + background: #3a2c0a; + border: 2px solid #f5c518; + color: #f5c518; +} + .message.success { background: #0f3d1a; border: 1px solid var(--color-success); color: var(--color-success); } +/* ── App layout ── */ +body.app { + display: block; + min-height: 100vh; + background: var(--color-bg); + padding: 1.5rem; +} + +.app-wrapper { + max-width: 1400px; + margin: 0 auto; + display: flex; + gap: 1.5rem; + align-items: stretch; + min-height: calc(100vh - 3rem); +} + +.main { + flex: 1; + min-width: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; +} + +.content { padding: 2rem 1.5rem; flex: 1; } + +/* ── Sidebar ── */ +.sidebar { + width: 240px; + flex-shrink: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-self: flex-start; + position: sticky; + top: 1.5rem; + max-height: calc(100vh - 3rem); + overflow-y: auto; + z-index: 10; + transition: transform 0.25s ease; +} + +.sidebar-icon-area { + display: flex; + justify-content: center; + padding: 1.25rem 1.25rem 0.75rem; + flex-shrink: 0; +} + +.sidebar-icon-area a { display: block; line-height: 0; } + +.sidebar-icon-area img { + width: 64px; + height: 64px; + object-fit: contain; + transition: transform 0.2s; +} + +.sidebar-icon-area img:hover { transform: scale(1.1); } + +.sidebar-mobile-only { display: none; } + +.sidebar ul { list-style: none; padding: 0.5rem 0; } + +.sidebar ul li a { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.7rem 1.25rem; + color: var(--color-text); + text-decoration: none; + font-size: 0.95rem; + border-left: 3px solid transparent; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.sidebar ul li a:hover, +.sidebar ul li a.active { + background: var(--color-secondary); + color: var(--color-primary); + border-left-color: var(--color-primary); +} + +.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; } + +.sidebar-profile-img { + width: 1.4rem; + height: 1.4rem; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; + border: 1px solid var(--color-secondary); +} + +/* ── Burger (mobile only) ── */ +.burger { + display: none; + position: fixed; + top: 0.75rem; right: 0.75rem; + background: var(--color-card); + border: 1px solid var(--color-secondary); + border-radius: 6px; + cursor: pointer; + color: var(--color-text); + padding: 0.35rem 0.5rem; + z-index: 110; + transition: background 0.15s; +} + +.burger:hover { background: var(--color-secondary); } + +.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; } + +.burger-icon span { + display: block; + height: 2px; + background: var(--color-text); + border-radius: 2px; + transition: transform 0.25s, opacity 0.25s; +} + +.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); } +.burger.open .burger-icon span:nth-child(2) { opacity: 0; } +.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + +/* ── Sidebar overlay ── */ +.sidebar-overlay { + display: none; + position: fixed; inset: 0; + background: rgba(0, 0, 0, 0.55); + z-index: 90; +} + +.sidebar-overlay.visible { display: block; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + body.app { padding: 0; } + + .app-wrapper { + flex-direction: column; + gap: 0; + min-height: 100vh; + } + + .sidebar { + position: fixed; + top: 0; right: 0; + width: 240px; + height: 100vh; + max-height: 100vh; + border-radius: 0; + border: none; + border-left: 1px solid var(--color-secondary); + box-shadow: none; + transform: translateX(100%); + align-self: auto; + z-index: 100; + } + + .sidebar.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); } + .sidebar-icon-area { display: none; } + + .main { + border-radius: 0; + box-shadow: none; + border: none; + min-height: 100vh; + } + + .burger { display: flex; } + .sidebar-mobile-only { display: block; } +} + +/* ── Social Sidebar ── */ +.social-sidebar { + width: 220px; + flex-shrink: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-self: flex-start; + position: sticky; + top: 1.5rem; + max-height: calc(100vh - 3rem); + overflow-y: auto; +} + +.social-sidebar ul { list-style: none; padding: 0.5rem 0; } + +.social-sidebar ul li a { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.7rem 1.25rem; + color: var(--color-text); + text-decoration: none; + font-size: 0.95rem; + border-left: 3px solid transparent; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.social-sidebar ul li a:hover, +.social-sidebar ul li a.active { + background: var(--color-secondary); + color: var(--color-primary); + border-left-color: var(--color-primary); +} + +.social-sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; } + +.social-sidebar-title { + padding: 1rem 1.25rem 0.25rem; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--color-muted); + text-transform: uppercase; + flex-shrink: 0; +} + +.social-badge { + margin-left: auto; + background: var(--color-primary); + color: #fff; + font-size: 0.7rem; + font-weight: 700; + border-radius: 9999px; + padding: 0.1rem 0.4rem; + line-height: 1.4; + flex-shrink: 0; +} + +@media (max-width: 768px) { + .social-sidebar { + position: static; + width: 100%; + max-height: none; + border-radius: 0; + border: none; + border-top: 1px solid var(--color-secondary); + box-shadow: none; + align-self: auto; + } +} + /* ── Token box ── */ .token-box { margin-top: 1.25rem; @@ -161,3 +423,38 @@ button.secondary:hover { color: #666; margin-bottom: 0.4rem; } + +/* ── Sidebar groups ── */ +.sidebar-group-toggle { + cursor: pointer; + justify-content: space-between; +} + +.sidebar-arrow { + margin-left: auto; + font-size: 0.7rem; + flex-shrink: 0; + transition: transform 0.2s; +} + +.sidebar-group.open > a .sidebar-arrow { + transform: rotate(90deg); +} + +.sidebar-sub { + list-style: none; + padding: 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.sidebar-group.open > .sidebar-sub { + max-height: 500px; +} + +.sidebar .sidebar-sub li a { + padding: 0.55rem 1.25rem 0.55rem 2.5rem; + font-size: 0.9rem; + color: var(--color-muted); +} diff --git a/xxxthegame/src/main/resources/static/entdecken.html b/xxxthegame/src/main/resources/static/entdecken.html index c1e2e36..663ddd0 100644 --- a/xxxthegame/src/main/resources/static/entdecken.html +++ b/xxxthegame/src/main/resources/static/entdecken.html @@ -7,70 +7,6 @@ - + -
- -
- - -
-
- -

Entdecken

-
- -
+
+
-
+ diff --git a/xxxthegame/src/main/resources/static/forgot-password.html b/xxxthegame/src/main/resources/static/forgot-password.html new file mode 100644 index 0000000..ecaa66a --- /dev/null +++ b/xxxthegame/src/main/resources/static/forgot-password.html @@ -0,0 +1,112 @@ + + + + + + xXx Games – Passwort vergessen + + + + + +
+ Logo +

Passwort vergessen

+

Gib deine E-Mail-Adresse ein. Falls sie bei uns registriert ist, erhältst du einen Link zum Zurücksetzen.

+ + + + + + +
+ +

+ Zurück zum Login +

+
+ +
+ +
+ + + + diff --git a/xxxthegame/src/main/resources/static/freunde.html b/xxxthegame/src/main/resources/static/freunde.html new file mode 100644 index 0000000..58ee3e8 --- /dev/null +++ b/xxxthegame/src/main/resources/static/freunde.html @@ -0,0 +1,278 @@ + + + + + + Freunde – XXX The Game + + + + + + +
+
+

Freunde

+ +
+ + +
+ + +
+
    + +
    + + +
    +
      + +
      +
      +
      + + + + + + diff --git a/xxxthegame/src/main/resources/static/icon.png b/xxxthegame/src/main/resources/static/icon.png new file mode 100644 index 0000000..da7b7fd Binary files /dev/null and b/xxxthegame/src/main/resources/static/icon.png differ diff --git a/xxxthegame/src/main/resources/static/img/lvl1.png b/xxxthegame/src/main/resources/static/img/lvl1.png new file mode 100644 index 0000000..3bde9b7 Binary files /dev/null and b/xxxthegame/src/main/resources/static/img/lvl1.png differ diff --git a/xxxthegame/src/main/resources/static/img/lvl2.png b/xxxthegame/src/main/resources/static/img/lvl2.png new file mode 100644 index 0000000..bcf3a9d Binary files /dev/null and b/xxxthegame/src/main/resources/static/img/lvl2.png differ diff --git a/xxxthegame/src/main/resources/static/img/lvl3.png b/xxxthegame/src/main/resources/static/img/lvl3.png new file mode 100644 index 0000000..c9a3e29 Binary files /dev/null and b/xxxthegame/src/main/resources/static/img/lvl3.png differ diff --git a/xxxthegame/src/main/resources/static/img/lvl4.png b/xxxthegame/src/main/resources/static/img/lvl4.png new file mode 100644 index 0000000..88ba411 Binary files /dev/null and b/xxxthegame/src/main/resources/static/img/lvl4.png differ diff --git a/xxxthegame/src/main/resources/static/img/lvl5.png b/xxxthegame/src/main/resources/static/img/lvl5.png new file mode 100644 index 0000000..72e16e4 Binary files /dev/null and b/xxxthegame/src/main/resources/static/img/lvl5.png differ diff --git a/xxxthegame/src/main/resources/static/index.html b/xxxthegame/src/main/resources/static/index.html index efc090f..e913fb1 100644 --- a/xxxthegame/src/main/resources/static/index.html +++ b/xxxthegame/src/main/resources/static/index.html @@ -3,14 +3,16 @@ - XXX The Game + xXx Games + -

      XXX The Game

      -

      Das Erwachsenenspiel für Paare und Gruppen

      -
      + Logo +

      Erwachsenenspiele

      +

      Alleine, als Paar oder mehr...

      + @@ -22,6 +24,18 @@ + + diff --git a/xxxthegame/src/main/resources/static/infochastity.html b/xxxthegame/src/main/resources/static/infochastity.html new file mode 100644 index 0000000..15561a7 --- /dev/null +++ b/xxxthegame/src/main/resources/static/infochastity.html @@ -0,0 +1,19 @@ + + + + + + Chastity Game – Info – XXX The Game + + + + +
      +
      +

      Chastity Game

      +

      Informationen zum Chastity Game folgen hier.

      +
      +
      + + + diff --git a/xxxthegame/src/main/resources/static/infovanilla.html b/xxxthegame/src/main/resources/static/infovanilla.html new file mode 100644 index 0000000..cb18d9d --- /dev/null +++ b/xxxthegame/src/main/resources/static/infovanilla.html @@ -0,0 +1,19 @@ + + + + + + Vanilla Game – Info – XXX The Game + + + + +
      +
      +

      Vanilla Game

      +

      Informationen zum Vanilla Game folgen hier.

      +
      +
      + + + diff --git a/xxxthegame/src/main/resources/static/js/sidebar.js b/xxxthegame/src/main/resources/static/js/sidebar.js new file mode 100644 index 0000000..f8d1559 --- /dev/null +++ b/xxxthegame/src/main/resources/static/js/sidebar.js @@ -0,0 +1,146 @@ +(function () { + const path = window.location.pathname; + + const groups = [ + { + label: 'Vanilla Game', + icon: '♡', + items: [ + { href: '/infovanilla.html', icon: 'ℹ', label: 'Info' }, + { href: '/sessionvanilla.html', icon: '▷', label: 'Neue Session' }, + ] + }, + { + label: 'BDSM Game', + icon: '◆', + items: [ + { href: '/infobdsm.html', icon: 'ℹ', label: 'Info' }, + { href: '/sessionbdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' }, + { href: '/sessionbdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' }, + { href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' }, + { href: '/toys.html', icon: '◈', label: 'Toys' }, + { href: '/entdecken.html', icon: '⊙', label: 'Entdecken' }, + ] + }, + { + label: 'Chastity Game', + icon: '⊗', + items: [ + { href: '/infochastity.html', icon: 'ℹ', label: 'Info' }, + { href: '/sessionchastity.html', icon: '▷', label: 'Neue Session' }, + ] + }, + ]; + + const homeCls = path === '/userhome.html' ? ' class="active"' : ''; + const homeItem = ` + `; + + const nav = groups.map(({ label, icon, items }) => { + const isOpen = items.some(item => item.href === path); + const openCls = isOpen ? ' open' : ''; + const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => { + const cls = path === href ? ' class="active"' : ''; + const idAt = iId ? ` id="${iId}"` : ''; + return `${iIcon} ${iLabel}`; + }).join(''); + return ` + `; + }).join(''); + + document.body.insertAdjacentHTML('afterbegin', ` + + + + `); + + // Sidebar und .main in einen zentrierten App-Wrapper verschieben + const appWrapper = document.createElement('div'); + appWrapper.className = 'app-wrapper'; + const sidebarEl = document.getElementById('sidebar'); + const mainEl = document.querySelector('.main'); + document.body.insertBefore(appWrapper, sidebarEl); + appWrapper.appendChild(sidebarEl); + if (mainEl) appWrapper.appendChild(mainEl); + + // Group toggle + document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => { + toggle.addEventListener('click', e => { + e.preventDefault(); + toggle.closest('.sidebar-group').classList.toggle('open'); + }); + }); + + // "Im Spiel" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet + const navNeu = document.getElementById('navBdsmNeu'); + const navImSpiel = document.getElementById('navBdsmImSpiel'); + if (navImSpiel) navImSpiel.style.display = 'none'; + + // Session-Status prüfen + fetch('/login/me') + .then(r => r.ok ? r.json() : null) + .then(async user => { + if (!user) return; + + // Session-Status prüfen und Menü anpassen + try { + const sessionRes = await fetch(`/session?userId=${user.userId}`); + const hasSession = sessionRes.status === 200; + if (navNeu) navNeu.style.display = hasSession ? 'none' : ''; + if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none'; + } catch (_) { /* Menü bleibt im Standardzustand */ } + }) + .catch(() => {}); + + const sidebar = document.getElementById('sidebar'); + const burgerBtn = document.getElementById('burgerBtn'); + const overlay = document.getElementById('sidebarOverlay'); + + function openMenu() { + sidebar.classList.add('open'); + overlay.classList.add('visible'); + burgerBtn.classList.add('open'); + burgerBtn.setAttribute('aria-label', 'Menü schließen'); + } + + function closeMenu() { + sidebar.classList.remove('open'); + overlay.classList.remove('visible'); + burgerBtn.classList.remove('open'); + burgerBtn.setAttribute('aria-label', 'Menü öffnen'); + } + + burgerBtn.addEventListener('click', () => + sidebar.classList.contains('open') ? closeMenu() : openMenu() + ); + overlay.addEventListener('click', closeMenu); + sidebar.querySelectorAll('a:not([href="/login/logout"]):not(.sidebar-group-toggle)').forEach(l => + l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }) + ); + + // Social sidebar auf allen App-Seiten nachladen + const s = document.createElement('script'); + s.src = '/js/social-sidebar.js'; + document.head.appendChild(s); +})(); diff --git a/xxxthegame/src/main/resources/static/js/social-sidebar.js b/xxxthegame/src/main/resources/static/js/social-sidebar.js new file mode 100644 index 0000000..7efc208 --- /dev/null +++ b/xxxthegame/src/main/resources/static/js/social-sidebar.js @@ -0,0 +1,111 @@ +(function () { + // Verhindert doppelte Ausführung (z.B. wenn sidebar.js nachladen UND direktes + + + + diff --git a/xxxthegame/src/main/resources/static/personen-suchen.html b/xxxthegame/src/main/resources/static/personen-suchen.html new file mode 100644 index 0000000..7cc0817 --- /dev/null +++ b/xxxthegame/src/main/resources/static/personen-suchen.html @@ -0,0 +1,204 @@ + + + + + + Personen suchen – XXX The Game + + + + + + +
      +
      +

      Personen suchen

      + + + +
        +

        Gib mindestens 2 Zeichen ein, um zu suchen.

        +
        +
        + + + + + + diff --git a/xxxthegame/src/main/resources/static/profile.html b/xxxthegame/src/main/resources/static/profile.html new file mode 100644 index 0000000..7207eb2 --- /dev/null +++ b/xxxthegame/src/main/resources/static/profile.html @@ -0,0 +1,646 @@ + + + + + + Profil – XXX The Game + + + + + + +
        +
        + +
        +
        + +
        + +
        + +
        +

        + +
        +
        + +
        + +
        +

        + +
        +
        + +
        + +
        + +
        + + + + + +
        +
        +
        + + + + + + + + + + + + + + diff --git a/xxxthegame/src/main/resources/static/registration.html b/xxxthegame/src/main/resources/static/registration.html index 20be204..33ed5e0 100644 --- a/xxxthegame/src/main/resources/static/registration.html +++ b/xxxthegame/src/main/resources/static/registration.html @@ -3,14 +3,14 @@ - XXX The Game – Registrierung + xXx Games – Neues Konto erstellen
        -

        XXX The Game

        -

        Neues Konto erstellen

        + Logo +

        Neues Konto erstellen

        @@ -84,6 +84,10 @@ showMessage('Diese E-Mail-Adresse ist bereits registriert.', 'error'); btn.disabled = false; btn.textContent = 'Registrieren'; + } else if (response.status === 409) { + showMessage('Dieser Name ist bereits vergeben.', 'error'); + btn.disabled = false; + btn.textContent = 'Registrieren'; } else { showMessage(`Fehler: HTTP ${response.status}`, 'error'); btn.disabled = false; diff --git a/xxxthegame/src/main/resources/static/reset-password.html b/xxxthegame/src/main/resources/static/reset-password.html new file mode 100644 index 0000000..90b8740 --- /dev/null +++ b/xxxthegame/src/main/resources/static/reset-password.html @@ -0,0 +1,150 @@ + + + + + + xXx Games – Neues Passwort + + + + + +
        + Logo +

        Neues Passwort

        +

        Gib dein neues Passwort ein.

        + + + + + + + + + +
        +
        + +
        + +
        + + + + diff --git a/xxxthegame/src/main/resources/static/sessionbdsm.html b/xxxthegame/src/main/resources/static/sessionbdsm.html new file mode 100644 index 0000000..0084fc4 --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionbdsm.html @@ -0,0 +1,297 @@ + + + + + + BDSM Game – Neue Session – XXX The Game + + + + + + + + +
        +
        + +

        BDSM Game

        +

        Schritt 1 von 4 – Session-Einstellungen

        + +
        +

        Session-Einstellungen

        + +
        +
        + + 15 % +
        + +
        + +
        +
        + + 15 % +
        + +
        + + + +
        +
        + + 5 +
        + +
        + +
        +
        + + 1,0 +
        + +
        +
        + +
        + + +
        +
        + + + + + diff --git a/xxxthegame/src/main/resources/static/sessionbdsmingame.html b/xxxthegame/src/main/resources/static/sessionbdsmingame.html new file mode 100644 index 0000000..af5346e --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionbdsmingame.html @@ -0,0 +1,588 @@ + + + + + + BDSM Game – Im Spiel – XXX The Game + + + + + + + + + +
        +
        + + + + + +
        +
        Aufgabe wird geladen…
        + +
        + +
        +
        + + + + + diff --git a/xxxthegame/src/main/resources/static/sessionbdsmplayers.html b/xxxthegame/src/main/resources/static/sessionbdsmplayers.html new file mode 100644 index 0000000..9e123c4 --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionbdsmplayers.html @@ -0,0 +1,438 @@ + + + + + + BDSM Game – Mitspieler – XXX The Game + + + + + +
        +
        + +

        BDSM Game

        +

        Schritt 2 von 4 – Mitspieler

        + +
        +

        Mitspieler

        +
        + +
        + +
        +
        + + +
        + +
        +
        + + + + + diff --git a/xxxthegame/src/main/resources/static/sessionbdsmtasks.html b/xxxthegame/src/main/resources/static/sessionbdsmtasks.html new file mode 100644 index 0000000..fb204d7 --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionbdsmtasks.html @@ -0,0 +1,324 @@ + + + + + + BDSM Game – Aufgaben-Gruppen – XXX The Game + + + + + +
        +
        + +

        BDSM Game

        +

        Schritt 3 von 4 – Aufgaben

        + +
        +

        +
          +
          + +
          +

          +
            +
            + +
            +

            +
              +
              + +
              +
              +
              + + +
              +
              + +
              +
              + + + + + diff --git a/xxxthegame/src/main/resources/static/sessionbdsmtoys.html b/xxxthegame/src/main/resources/static/sessionbdsmtoys.html new file mode 100644 index 0000000..d95c00e --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionbdsmtoys.html @@ -0,0 +1,312 @@ + + + + + + BDSM Game – Toys – XXX The Game + + + + + +
              +
              + +

              BDSM Game

              +

              Schritt 4 von 4 – Toys

              + +
              +

              Benötigte Toys

              +

              + Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt. +

              +
              +
              + +
              +
              +
              + + +
              +
              + +
              +
              + + + + + diff --git a/xxxthegame/src/main/resources/static/sessionchastity.html b/xxxthegame/src/main/resources/static/sessionchastity.html new file mode 100644 index 0000000..da5ef65 --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionchastity.html @@ -0,0 +1,19 @@ + + + + + + Chastity Game – Neue Session – XXX The Game + + + + +
              +
              +

              Chastity Game – Neue Session

              +

              Session-Setup für das Chastity Game folgt hier.

              +
              +
              + + + diff --git a/xxxthegame/src/main/resources/static/sessionvanilla.html b/xxxthegame/src/main/resources/static/sessionvanilla.html new file mode 100644 index 0000000..bf2c377 --- /dev/null +++ b/xxxthegame/src/main/resources/static/sessionvanilla.html @@ -0,0 +1,19 @@ + + + + + + Vanilla Game – Neue Session – XXX The Game + + + + +
              +
              +

              Vanilla Game – Neue Session

              +

              Session-Setup für das Vanilla Game folgt hier.

              +
              +
              + + + diff --git a/xxxthegame/src/main/resources/static/toys.html b/xxxthegame/src/main/resources/static/toys.html index 4ef3187..2502f53 100644 --- a/xxxthegame/src/main/resources/static/toys.html +++ b/xxxthegame/src/main/resources/static/toys.html @@ -7,103 +7,6 @@ - - -
              +
              -
              - - - -
              -
              - -

              Toys

              -
              - -
              +
              +
              @@ -418,13 +266,9 @@
              -
              Wird geladen…
              - + +
              @@ -436,58 +280,130 @@
              -
              Wird geladen…
              - + +
              -
              + diff --git a/xxxthegame/src/main/resources/static/userhome.html b/xxxthegame/src/main/resources/static/userhome.html index ff88286..f78f313 100644 --- a/xxxthegame/src/main/resources/static/userhome.html +++ b/xxxthegame/src/main/resources/static/userhome.html @@ -6,274 +6,27 @@ XXX The Game - - + -
              - -
              - - - -
              -
              - -

              XXX The Game

              -
              - -
              -

              -
              +
              +
              + Logo +

              -
              +