diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ec2dde6..2e0847d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,8 @@ "Bash(for f:*)", "Bash(ls:*)", "Bash(./gradlew compileJava)", - "Bash(./gradlew build:*)" + "Bash(./gradlew build:*)", + "Bash(find /home/mario/Workspaces/xxx-thegame -type f \\\\\\(-name *bdsm* -o -name *BDSM* \\\\\\))" ] } } diff --git a/.metadata/.lock_info b/.metadata/.lock_info index e89fb92..258f63c 100644 --- a/.metadata/.lock_info +++ b/.metadata/.lock_info @@ -1,5 +1,5 @@ -#Thu Mar 19 23:00:53 CET 2026 +#Fri Mar 20 15:47:06 CET 2026 display=\:0 host=Mario-Linux -process-id=50461 +process-id=112524 user=mario diff --git a/.metadata/.log b/.metadata/.log index 405cad2..36e8649 100644 --- a/.metadata/.log +++ b/.metadata/.log @@ -3416,3 +3416,616 @@ java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plu at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-20 00:43:19.865 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 00:43:20.572 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) +!SESSION 2026-03-20 07:44:39.589 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.10 +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-20 07:44:40.568 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-20 07:44:42.780 +!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-20 07:44:42.926 +!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-20 07:44:42.926 +!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-20 07:44:43.066 +!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-20 07:44:43.066 +!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.workbench 4 0 2026-03-20 07:44:54.556 +!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception +!STACK 0 +org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)) + at org.eclipse.swt.SWT.error(SWT.java:4950) + at org.eclipse.swt.SWT.error(SWT.java:4865) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207) + at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198) + at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38) + at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76) + at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83) + at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57) + at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194) + at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58) + at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183) + at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136) + at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950) + at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34) + at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65) + at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1) + at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230) + at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151) + at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132) + at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73) + at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48) + at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55) + at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61) + at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424) + at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365) + at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111) + at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89) + at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467) + at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493) + at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297) + at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91) + at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661) + at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290) + at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051) + at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153) + at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583) + at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173) + at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185) + at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615) + at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563) + at org.eclipse.equinox.launcher.Main.run(Main.java:1415) + at org.eclipse.equinox.launcher.Main.main(Main.java:1387) +Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at java.base/java.io.FileInputStream.(FileInputStream.java:106) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204) + ... 68 more +!SESSION 2026-03-20 10:34:12.527 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.10 +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-20 10:34:13.217 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY org.eclipse.core.resources 2 10035 2026-03-20 10:34:15.534 +!MESSAGE The workspace exited with unsaved changes in the previous session; refreshing workspace to recover changes. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-20 10:34:15.774 +!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-20 10:34:15.901 +!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-20 10:34:15.901 +!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-20 10:34:16.018 +!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-20 10:34:16.018 +!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.workbench 4 0 2026-03-20 11:05:16.162 +!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception +!STACK 0 +org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)) + at org.eclipse.swt.SWT.error(SWT.java:4950) + at org.eclipse.swt.SWT.error(SWT.java:4865) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207) + at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198) + at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38) + at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76) + at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83) + at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57) + at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194) + at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58) + at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183) + at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136) + at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950) + at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34) + at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65) + at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1) + at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230) + at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151) + at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132) + at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73) + at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48) + at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55) + at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61) + at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424) + at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365) + at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111) + at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89) + at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467) + at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493) + at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297) + at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91) + at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661) + at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290) + at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051) + at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153) + at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583) + at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173) + at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185) + at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615) + at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563) + at org.eclipse.equinox.launcher.Main.run(Main.java:1415) + at org.eclipse.equinox.launcher.Main.main(Main.java:1387) +Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at java.base/java.io.FileInputStream.(FileInputStream.java:106) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204) + ... 68 more +!SESSION 2026-03-20 12:07:51.165 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.10 +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-20 12:07:51.857 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY org.eclipse.core.resources 2 10035 2026-03-20 12:07:59.028 +!MESSAGE The workspace exited with unsaved changes in the previous session; refreshing workspace to recover changes. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-20 12:07:59.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-20 12:07:59.406 +!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-20 12:07:59.406 +!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-20 12:07:59.525 +!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-20 12:07:59.525 +!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.workbench 4 0 2026-03-20 12:57:37.530 +!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception +!STACK 0 +org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)) + at org.eclipse.swt.SWT.error(SWT.java:4950) + at org.eclipse.swt.SWT.error(SWT.java:4865) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207) + at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198) + at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38) + at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76) + at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83) + at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57) + at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194) + at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58) + at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183) + at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136) + at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950) + at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34) + at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65) + at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1) + at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230) + at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151) + at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132) + at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73) + at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48) + at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55) + at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61) + at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424) + at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365) + at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111) + at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89) + at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467) + at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493) + at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297) + at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91) + at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661) + at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290) + at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051) + at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153) + at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583) + at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173) + at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185) + at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615) + at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563) + at org.eclipse.equinox.launcher.Main.run(Main.java:1415) + at org.eclipse.equinox.launcher.Main.main(Main.java:1387) +Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at java.base/java.io.FileInputStream.(FileInputStream.java:106) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204) + ... 68 more + +!ENTRY org.eclipse.ui.workbench 4 0 2026-03-20 13:10:19.604 +!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception +!STACK 0 +org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)) + at org.eclipse.swt.SWT.error(SWT.java:4950) + at org.eclipse.swt.SWT.error(SWT.java:4865) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207) + at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198) + at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38) + at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76) + at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83) + at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57) + at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194) + at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804) + at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58) + at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183) + at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136) + at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950) + at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34) + at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65) + at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201) + at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1) + at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230) + at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151) + at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132) + at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73) + at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48) + at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55) + at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61) + at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424) + at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365) + at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243) + at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111) + at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89) + at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467) + at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493) + at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297) + at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91) + at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682) + at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661) + at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290) + at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051) + at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153) + at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684) + at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339) + at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583) + at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173) + at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185) + at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149) + at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467) + at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615) + at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563) + at org.eclipse.equinox.launcher.Main.run(Main.java:1415) + at org.eclipse.equinox.launcher.Main.main(Main.java:1387) +Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at java.base/java.io.FileInputStream.(FileInputStream.java:106) + at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204) + ... 68 more + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 14:51:12.457 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:34.279 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:39.025 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:48.762 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:19.558 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:43.890 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:51.083 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jface 2 0 2026-03-20 15:42:01.796 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-20 15:42:01.796 +!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.springframework.tooling.boot.ls 1 0 2026-03-20 15:47:03.456 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS + +!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:47:03.843 +!MESSAGE Failed to save JDT index: Index for /xxxthegame +!STACK 0 +java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83) + at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633) + at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536) + at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178) + at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) + at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) + at java.base/java.lang.Thread.run(Thread.java:1583) +!SESSION 2026-03-20 15:47:04.884 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.10 +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 -data file:/home/mario/Workspaces/xxx-thegame/ + +!ENTRY ch.qos.logback.classic 1 0 2026-03-20 15:47:05.724 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-20 15:47:06.276 +!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-20 15:47:06.437 +!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-20 15:47:06.437 +!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-20 15:47:06.565 +!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-20 15:47:06.565 +!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.springframework.tooling.boot.ls 1 0 2026-03-20 16:01:51.334 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS diff --git a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json index 5499258..d7e4abe 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json +++ b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json @@ -1,7 +1,7 @@ [ { - "version" : "9.4.1-20260319034812+0000", - "buildTime" : "20260319034812+0000", - "commitId" : "2d6327017519d23b96af35865dc997fcb544fb40", + "version" : "9.5.0-20260320031124+0000", + "buildTime" : "20260320031124+0000", + "commitId" : "97faa73152fb6d4ea37edf6b3f7590dcbce8b952", "current" : false, "snapshot" : true, "nightly" : false, @@ -10,15 +10,15 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-bin.zip.sha256", - "checksum" : "a1f30c1e81a9e33725a213f158d5044dd305f438e539983f683f58b5860ab65e", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-wrapper.jar.sha256", - "wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-bin.zip.sha256", + "checksum" : "d28982b60bd15c7f3e13032152fc384f30465713b9c439bd3e159ad758461393", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-wrapper.jar.sha256", + "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" }, { - "version" : "9.5.0-20260319005705+0000", - "buildTime" : "20260319005705+0000", - "commitId" : "312894732cc8829c4f69bd292c9b259a1f5bfd8f", + "version" : "9.6.0-20260319194115+0000", + "buildTime" : "20260319194115+0000", + "commitId" : "eaac62111b6cbb05984176b52e4be56d5249ebf8", "current" : false, "snapshot" : true, "nightly" : true, @@ -27,11 +27,28 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-bin.zip.sha256", - "checksum" : "9c1f5565f97acfcbfd7b6e2a0be3eb65f366f8d522b0c82f82839d08fd8d3aaf", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-wrapper.jar.sha256", - "wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8" + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-bin.zip.sha256", + "checksum" : "a72c6e0f1a5ecc7d81768c65a5bdcd8f0af37ba5b05c83df4c45d08b8ce79fce", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-wrapper.jar.sha256", + "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" +}, { + "version" : "9.4.1", + "buildTime" : "20260319084628+0000", + "commitId" : "2d6327017519d23b96af35865dc997fcb544fb40", + "current" : true, + "snapshot" : false, + "nightly" : false, + "releaseNightly" : false, + "activeRc" : false, + "rcFor" : "", + "milestoneFor" : "", + "broken" : false, + "downloadUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-bin.zip.sha256", + "checksum" : "2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-wrapper.jar.sha256", + "wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" }, { "version" : "9.5.0-milestone-7", "buildTime" : "20260315084051+0000", @@ -53,7 +70,7 @@ "version" : "9.4.0", "buildTime" : "20260304103600+0000", "commitId" : "b631911858264c0b6e4d6603d677ff5218766cee", - "current" : true, + "current" : false, "snapshot" : false, "nightly" : false, "releaseNightly" : false, diff --git a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/xxxthegame index d7455f9..aec484e 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 @@ # -#Thu Mar 19 23:00:50 CET 2026 +#Fri Mar 20 16:01:51 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\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/.markers b/.metadata/.plugins/org.eclipse.core.resources/.projects/xxxthegame/.markers index 1d2b0b2..cffed5f 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 e1642f1..6366665 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.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi index 4328b62..aa367bc 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 @@ -84,133 +84,129 @@ 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 + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - - + + View categoryTag:Other - - + + 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 Oomph Gradle Debug - + 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:Debug busy - - View - categoryTag:Oomph - NoRestore - - + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -259,100 +255,100 @@ 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 active - + 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 - + View categoryTag:General - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Debug - + View categoryTag:General @@ -361,2719 +357,2789 @@ - - + + View categoryTag:Help - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:Help - + View categoryTag:General - + 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 - + 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.ui.DefaultTextEditor - active - - + + 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 + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + Editor + removeOnHide + org.eclipse.ui.genericeditor.GenericEditor + + + + 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.ClassFileEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor + - + View categoryTag:Java - + active + activeOnClose + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + - + 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:Other - + ViewMenu menuContribution:menu - + - + View categoryTag:Ant - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Gradle - + 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 - + - - - - - View - categoryTag:Oomph - NoRestore - - ViewMenu - menuContribution:menu - - - - - + + toolbarSeparator - + - + Draggable - + - + toolbarSeparator - + - + Draggable - - + + - + toolbarSeparator - + - + Draggable - + Draggable - + Draggable - + Draggable - + toolbarSeparator - + - + Draggable - + - - Draggable - - - Draggable - - + toolbarSeparator - + - + toolbarSeparator - + - + Draggable - + stretch SHOW_RESTORE_MENU - + Draggable HIDEABLE SHOW_RESTORE_MENU - - + + stretch - + Draggable - + 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:Spring - + View categoryTag:Spring - + View categoryTag:Spring - - + + 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 f4e22bb..ebeb2da 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/externalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache index 8f63c03..68bf8e8 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache and b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps index bfb02ab..0ede2b3 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps and b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt index 327869c..41b818c 100644 --- a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt +++ b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt @@ -4,13 +4,14 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 176453541.index 677104696.index 341080888.index -774576701.index 4134502745.index +774576701.index 41199409.index 2217896880.index 134995224.index 4025319337.index 900586112.index +9341915.index 2929476459.index 2065500052.index 3051047092.index @@ -27,8 +28,8 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 2032345814.index 3839581777.index 2466743981.index -13999064.index 673436610.index +13999064.index 3972616808.index 1914043487.index 3154281632.index @@ -44,10 +45,10 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 4020783879.index 2900482015.index 3059431983.index -833027591.index 13156219.index -37241354.index +833027591.index 4088356365.index +37241354.index 1295630681.index 2701419231.index 3939420913.index @@ -55,35 +56,35 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 1318022262.index 773718761.index 2311226047.index -3539841425.index 1865797976.index +3539841425.index 2455962971.index -836138551.index +2576972120.index 2389383899.index 2226615777.index 3515611559.index -3728851734.index -2826242951.index 2899155238.index -3763224039.index +836138551.index 2138052223.index +3763224039.index +3728851734.index 2236377038.index 3547251881.index -371677185.index -2127778675.index -2519831052.index -1063231598.index -2874180664.index -2939623059.index -2576972120.index -2376429633.index -2628068441.index -1090991043.index 1138623861.index +2376429633.index +2519831052.index +371677185.index +2874180664.index +1090991043.index +2826242951.index +2127778675.index +2628068441.index +1063231598.index +2939623059.index 1223891870.index 3769604005.index -3158780236.index 2237645717.index +3158780236.index 2852275968.index 2403041570.index 1704193220.index @@ -95,12 +96,12 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec 352173590.index 766439048.index 3424266581.index -2247053514.index 1765772496.index -3514351073.index -3892622621.index 2494834982.index -1780956574.index -1022297761.index +3514351073.index +2247053514.index 1938594271.index +3892622621.index +1022297761.index +1780956574.index 1256436118.index diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml index 38978c3..11e3ba5 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml @@ -2,4 +2,6 @@ + + diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/0.log b/.metadata/.plugins/org.eclipse.m2e.logback/0.log index c4e1ad4..2319798 100644 --- a/.metadata/.plugins/org.eclipse.m2e.logback/0.log +++ b/.metadata/.plugins/org.eclipse.m2e.logback/0.log @@ -27,3 +27,7 @@ 2026-03-19 18:15:12,369 [Worker-5: 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-19 21:54:37,068 [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-19 23:00:56,635 [Worker-1: 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-20 07:44:45,034 [Worker-2: 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-20 10:34:17,517 [Worker-5: 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-20 12:08:01,037 [Worker-1: 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-20 15:47:08,644 [Worker-1: 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 3ef9565..9ede470 100644 --- a/.metadata/version.ini +++ b/.metadata/version.ini @@ -1,3 +1,3 @@ -#Thu Mar 19 23:00:53 CET 2026 +#Fri Mar 20 15:47:06 CET 2026 org.eclipse.core.runtime=2 org.eclipse.platform=4.39.0.v20260226-0420 diff --git a/bilder/logo_dating.png b/bilder/logo_dating.png new file mode 100644 index 0000000..d21c588 Binary files /dev/null and b/bilder/logo_dating.png differ diff --git a/bilder/logo_dating_transparent.png b/bilder/logo_dating_transparent.png new file mode 100644 index 0000000..59040dd Binary files /dev/null and b/bilder/logo_dating_transparent.png differ diff --git a/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppeService.java b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppeService.java new file mode 100644 index 0000000..eb069d4 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/aufgaben/AufgabenGruppeService.java @@ -0,0 +1,190 @@ +package de.oaa.xxx.aufgaben; + +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.SperreRepository; +import de.oaa.xxx.aufgaben.repository.StrafeRepository; +import de.oaa.xxx.aufgaben.repository.ToyRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Service für komplexe AufgabenGruppen-Operationen. + * Kapselt die Kopier-Logik (Systemgruppe → eigene Gruppe) inkl. Toy-Mapping. + */ +@Service +public class AufgabenGruppeService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeService.class); + + private final AufgabenGruppeRepository gruppeRepository; + private final AufgabeRepository aufgabeRepository; + private final StrafeRepository strafeRepository; + private final SperreRepository sperreRepository; + private final FinisherRepository finisherRepository; + private final ToyRepository toyRepository; + + public AufgabenGruppeService(AufgabenGruppeRepository gruppeRepository, + AufgabeRepository aufgabeRepository, + StrafeRepository strafeRepository, + SperreRepository sperreRepository, + FinisherRepository finisherRepository, + ToyRepository toyRepository) { + this.gruppeRepository = gruppeRepository; + this.aufgabeRepository = aufgabeRepository; + this.strafeRepository = strafeRepository; + this.sperreRepository = sperreRepository; + this.finisherRepository = finisherRepository; + this.toyRepository = toyRepository; + } + + /** + * Kopiert eine öffentliche (System-)Gruppe in die eigene Sammlung des Users. + * + * @param sourceId UUID der Quellgruppe + * @param userId UUID des Ziel-Users + * @return UUID der neu erstellten Gruppe + * @throws IllegalStateException wenn das Gruppen-Limit (10) erreicht ist + * @throws IllegalArgumentException wenn die Quellgruppe nicht gefunden oder nicht kopierbar ist + */ + @Transactional + public UUID copyGruppe(UUID sourceId, UUID userId) { + if (gruppeRepository.countByUserId(userId) >= 10) { + throw new IllegalStateException("Gruppen-Limit erreicht"); + } + + AufgabenGruppeEntity source = gruppeRepository.findById(sourceId) + .orElseThrow(() -> new IllegalArgumentException("Gruppe nicht gefunden: " + sourceId)); + if (source.isPrivateGruppe()) { + throw new IllegalArgumentException("Privat-Gruppen können nicht kopiert werden"); + } + if (userId.equals(source.getUserId())) { + throw new IllegalArgumentException("Eigene Gruppen können nicht kopiert werden"); + } + + // Toy-Mapping aufbauen: Source-ToyId → Ziel-ToyEntity + Set allSourceToys = new HashSet<>(); + 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) { + if (sourceToy.getUserId() == null) { + // System-Toy: direkt referenzieren + toyMapping.put(sourceToy.getToyId(), sourceToy); + } else { + // User-Toy: gleichnamiges Toy suchen oder kopieren + ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), userId) + .orElseGet(() -> { + ToyEntity tc = new ToyEntity(); + tc.setToyId(UUID.randomUUID()); + tc.setName(sourceToy.getName()); + tc.setBeschreibung(sourceToy.getBeschreibung()); + tc.setBild(sourceToy.getBild()); + tc.setUserId(userId); + return toyRepository.save(tc); + }); + toyMapping.put(sourceToy.getToyId(), mapped); + } + } + + // Neue Gruppe anlegen + AufgabenGruppeEntity copy = new AufgabenGruppeEntity(); + copy.setGruppenId(UUID.randomUUID()); + copy.setName(source.getName()); + copy.setBeschreibung(source.getBeschreibung()); + copy.setVon(source.getVon()); + copy.setBild(source.getBild()); + copy.setUserId(userId); + copy.setPrivateGruppe(true); + gruppeRepository.save(copy); + + // Aufgaben kopieren + for (AufgabeEntity a : source.getAufgaben()) { + AufgabeEntity ac = new AufgabeEntity(); + ac.setAufgabeId(UUID.randomUUID()); + ac.setAufgabenGruppe(copy); + ac.setKurzText(a.getKurzText()); + ac.setText(a.getText()); + ac.setLevel(a.getLevel()); + ac.setSekundenVon(a.getSekundenVon()); + ac.setSekundenBis(a.getSekundenBis()); + ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null); + ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null); + ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping)); + aufgabeRepository.save(ac); + } + + // Strafen kopieren + for (StrafeEntity s : source.getStrafen()) { + StrafeEntity sc = new StrafeEntity(); + sc.setStrafeId(UUID.randomUUID()); + sc.setAufgabenGruppe(copy); + sc.setKurzText(s.getKurzText()); + sc.setText(s.getText()); + sc.setLevel(s.getLevel()); + sc.setSekundenVon(s.getSekundenVon()); + sc.setSekundenBis(s.getSekundenBis()); + sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null); + sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null); + sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping)); + strafeRepository.save(sc); + } + + // Sperren kopieren + for (SperreEntity sp : source.getSperren()) { + SperreEntity spc = new SperreEntity(); + spc.setSperreId(UUID.randomUUID()); + spc.setAufgabenGruppe(copy); + spc.setKurzText(sp.getKurzText()); + spc.setText(sp.getText()); + spc.setReleaseText(sp.getReleaseText()); + spc.setMinutenVon(sp.getMinutenVon()); + spc.setMinutenBis(sp.getMinutenBis()); + spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null); + spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping)); + sperreRepository.save(spc); + } + + // Finisher kopieren + 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); + } + + LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", userId, copy.getGruppenId(), sourceId); + return copy.getGruppenId(); + } + + private List mapToys(List source, Map mapping) { + if (source == null || source.isEmpty()) return new ArrayList<>(); + return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList(); + } +} 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 0232d90..88b8f61 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 @@ -1,23 +1,9 @@ package de.oaa.xxx.aufgaben.controller; -import de.oaa.xxx.aufgaben.AufgabenGruppe; -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; -import de.oaa.xxx.aufgaben.repository.ToyRepository; -import de.oaa.xxx.user.UserEntity; -import de.oaa.xxx.user.UserRepository; +import java.security.Principal; +import java.util.Base64; +import java.util.UUID; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -37,15 +23,19 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Base64; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import de.oaa.xxx.aufgaben.AufgabenGruppe; +import de.oaa.xxx.aufgaben.AufgabenGruppeList; +import de.oaa.xxx.aufgaben.AufgabenGruppePage; +import de.oaa.xxx.aufgaben.AufgabenGruppeService; +import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; +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; +import de.oaa.xxx.user.UserEntity; +import de.oaa.xxx.user.UserRepository; @RestController @RequestMapping("/gruppe") @@ -62,7 +52,7 @@ public class AufgabenGruppeController { private final FinisherRepository finisherRepository; private final UserRepository userRepository; private final GruppenAboRepository aboRepository; - private final ToyRepository toyRepository; + private final AufgabenGruppeService aufgabenGruppeService; public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository, AufgabeRepository aufgabeRepository, @@ -71,7 +61,7 @@ public class AufgabenGruppeController { FinisherRepository finisherRepository, UserRepository userRepository, GruppenAboRepository aboRepository, - ToyRepository toyRepository) { + AufgabenGruppeService aufgabenGruppeService) { this.gruppeRepository = gruppeRepository; this.aufgabeRepository = aufgabeRepository; this.strafeRepository = strafeRepository; @@ -79,7 +69,7 @@ public class AufgabenGruppeController { this.finisherRepository = finisherRepository; this.userRepository = userRepository; this.aboRepository = aboRepository; - this.toyRepository = toyRepository; + this.aufgabenGruppeService = aufgabenGruppeService; } // ── Paginierte Listen ── @@ -190,118 +180,16 @@ public class AufgabenGruppeController { public ResponseEntity copy(@PathVariable UUID gruppeId, Principal principal) { UserEntity user = resolveUser(principal); if (user == null) return ResponseEntity.status(401).build(); - - if (gruppeRepository.countByUserId(user.getUserId()) >= 10) { + try { + aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId()); + return ResponseEntity.status(201).build(); + } catch (IllegalStateException e) { return ResponseEntity.status(409).build(); + } catch (IllegalArgumentException e) { + String msg = e.getMessage(); + if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build(); + return ResponseEntity.status(403).build(); } - - AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null); - if (source == null) return ResponseEntity.notFound().build(); - if (source.isPrivateGruppe()) return ResponseEntity.status(403).build(); - if (user.getUserId().equals(source.getUserId())) return ResponseEntity.status(403).build(); - - // Build toy mapping: source toyId → toy entity the copy will reference - Set allSourceToys = new HashSet<>(); - 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) { - if (sourceToy.getUserId() == null) { - // System toy – reference directly - toyMapping.put(sourceToy.getToyId(), sourceToy); - } else { - // User toy – find existing toy with same name in user's collection, or create a copy - ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), user.getUserId()) - .orElseGet(() -> { - ToyEntity tc = new ToyEntity(); - tc.setToyId(UUID.randomUUID()); - tc.setName(sourceToy.getName()); - tc.setBeschreibung(sourceToy.getBeschreibung()); - tc.setBild(sourceToy.getBild()); - tc.setUserId(user.getUserId()); - return toyRepository.save(tc); - }); - toyMapping.put(sourceToy.getToyId(), mapped); - } - } - - AufgabenGruppeEntity copy = new AufgabenGruppeEntity(); - copy.setGruppenId(UUID.randomUUID()); - copy.setName(source.getName()); - copy.setBeschreibung(source.getBeschreibung()); - copy.setVon(source.getVon()); - copy.setBild(source.getBild()); - copy.setUserId(user.getUserId()); - copy.setPrivateGruppe(true); - gruppeRepository.save(copy); - - for (AufgabeEntity a : source.getAufgaben()) { - AufgabeEntity ac = new AufgabeEntity(); - ac.setAufgabeId(UUID.randomUUID()); - ac.setAufgabenGruppe(copy); - ac.setKurzText(a.getKurzText()); - ac.setText(a.getText()); - ac.setLevel(a.getLevel()); - ac.setSekundenVon(a.getSekundenVon()); - ac.setSekundenBis(a.getSekundenBis()); - ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null); - ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null); - ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping)); - aufgabeRepository.save(ac); - } - - for (StrafeEntity s : source.getStrafen()) { - StrafeEntity sc = new StrafeEntity(); - sc.setStrafeId(UUID.randomUUID()); - sc.setAufgabenGruppe(copy); - sc.setKurzText(s.getKurzText()); - sc.setText(s.getText()); - sc.setLevel(s.getLevel()); - sc.setSekundenVon(s.getSekundenVon()); - sc.setSekundenBis(s.getSekundenBis()); - sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null); - sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null); - sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping)); - strafeRepository.save(sc); - } - - for (SperreEntity sp : source.getSperren()) { - SperreEntity spc = new SperreEntity(); - spc.setSperreId(UUID.randomUUID()); - spc.setAufgabenGruppe(copy); - spc.setKurzText(sp.getKurzText()); - spc.setText(sp.getText()); - spc.setReleaseText(sp.getReleaseText()); - spc.setMinutenVon(sp.getMinutenVon()); - spc.setMinutenBis(sp.getMinutenBis()); - spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null); - spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping)); - 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); - } - - LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", user.getUserId(), copy.getGruppenId(), gruppeId); - return ResponseEntity.status(201).build(); - } - - private List mapToys(List source, Map mapping) { - if (source == null || source.isEmpty()) return new ArrayList<>(); - return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList(); } // ── Löschen ── 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 be981a4..4d97d4e 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java @@ -8,6 +8,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -49,6 +51,9 @@ public class SecurityConfig { .requestMatchers("/sessionbdsmtasks.html").authenticated() .requestMatchers("/sessionbdsmtoys.html").authenticated() .requestMatchers("/sessionbdsmingame.html").authenticated() + .requestMatchers("/neubdsm.html").authenticated() + .requestMatchers("/bdsmingame.html").authenticated() + .requestMatchers("/bdsmwarten.html").authenticated() .requestMatchers("/personen-suchen.html").authenticated() .requestMatchers("/freunde.html").authenticated() .requestMatchers("/nachrichten.html").authenticated() @@ -79,6 +84,7 @@ public class SecurityConfig { .requestMatchers("/*.svg").permitAll() .requestMatchers("/*.webp").permitAll() .requestMatchers(HttpMethod.GET, "/login").permitAll() + .requestMatchers(HttpMethod.POST, "/login").permitAll() .requestMatchers(HttpMethod.GET, "/login/publickey").permitAll() .requestMatchers(HttpMethod.GET, "/login/logout").permitAll() .requestMatchers(HttpMethod.POST, "/user").permitAll() @@ -96,4 +102,9 @@ public class SecurityConfig { .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java new file mode 100644 index 0000000..e1a0390 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java @@ -0,0 +1,208 @@ +package de.oaa.xxx.games.bdsm; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity; +import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository; +import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository; +import de.oaa.xxx.games.bdsm.repository.MitspielerRepository; +import de.oaa.xxx.games.chastity.cardlock.CardLockEntity; +import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; +import de.oaa.xxx.games.history.GameHistoryEntity; +import de.oaa.xxx.games.history.GameHistoryRepository; +import de.oaa.xxx.games.history.GameRole; +import de.oaa.xxx.games.history.GameType; +import de.oaa.xxx.social.SystemMessageService; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.user.UserRepository; + +/** + * Service für komplexe BDSM-Game-Operationen. + * Kapselt Spielabschluss-Logik (XP-Vergabe, History) und den BDSM→Chastity-Übergang. + */ +@Service +public class BdsmGameService { + + private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameService.class); + + private final BdsmGameRepository sessionRepository; + private final MitspielerRepository mitspielerRepository; + private final AktiveSperreRepository aktiveSperreRepository; + private final UserRepository userRepository; + private final GameHistoryRepository gameHistoryRepository; + private final CardlockRepository cardlockRepository; + private final SystemMessageService systemMessageService; + + public BdsmGameService(BdsmGameRepository sessionRepository, + MitspielerRepository mitspielerRepository, + AktiveSperreRepository aktiveSperreRepository, + UserRepository userRepository, + GameHistoryRepository gameHistoryRepository, + CardlockRepository cardlockRepository, + SystemMessageService systemMessageService) { + this.sessionRepository = sessionRepository; + this.mitspielerRepository = mitspielerRepository; + this.aktiveSperreRepository = aktiveSperreRepository; + this.userRepository = userRepository; + this.gameHistoryRepository = gameHistoryRepository; + this.cardlockRepository = cardlockRepository; + this.systemMessageService = systemMessageService; + } + + /** + * Beendet eine BDSM-Session ordentlich: History speichern, XP vergeben, + * Gäste auf eigenem Gerät benachrichtigen, Daten aufräumen. + */ + @Transactional + public void spielAbschliessen(BdsmGameEntity entity) { + LocalDateTime endTime = LocalDateTime.now(); + long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes(); + + GameHistoryEntity entry = new GameHistoryEntity(); + entry.setGameName("BDSM Game"); + entry.setGameType(GameType.BDSM); + entry.setStartTime(entity.getStartZeit()); + entry.setEndTime(endTime); + entry.setDurationMinutes(durationMinutes); + entry.addParticipant(entity.getUserId(), GameRole.PLAYER); + entity.getMitspieler().stream() + .filter(m -> m.getUserId() != null) + .forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER)); + gameHistoryRepository.save(entry); + + int xp = (int) durationMinutes; + userRepository.findById(entity.getUserId()).ifPresent(u -> { + u.setBdsmXp(u.getBdsmXp() + xp); + userRepository.save(u); + }); + entity.getMitspieler().stream() + .filter(m -> m.getUserId() != null) + .forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { + u.setBdsmXp(u.getBdsmXp() + xp); + userRepository.save(u); + })); + + // Gäste auf eigenem Gerät benachrichtigen + String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉"; + entity.getMitspieler().stream() + .filter(m -> m.isEigenesGeraet() && m.getUserId() != null) + .forEach(m -> systemMessageService.send(entity.getUserId(), m.getUserId(), + endNachricht, "/userhome.html", MessageCause.GAME_STATE)); + + bereinige(entity); + } + + /** + * Überführt eine BDSM-Session in ein neues Chastity-Lock (BDSM→Chastity-Transition). + * History + XP werden wie beim normalen Spielabschluss vergeben. + * + * @return Das neu angelegte CardLockEntity + * @throws IllegalArgumentException wenn Session oder Template nicht gefunden + * @throws IllegalStateException wenn Lockee bereits ein aktives Lock hat + */ + @Transactional + public CardLockEntity zuChastity(UUID sessionId, UUID templateLockId, UUID lockeeUserId, UUID keyholderUserId) { + BdsmGameEntity entity = sessionRepository.findById(sessionId) + .orElseThrow(() -> new IllegalArgumentException("Session nicht gefunden: " + sessionId)); + + CardLockEntity template = cardlockRepository.findById(templateLockId) + .orElseThrow(() -> new IllegalArgumentException("Template-Lock nicht gefunden: " + templateLockId)); + + if (lockeeUserId != null + && cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeUserId)) { + throw new IllegalStateException("Lockee hat bereits ein aktives Chastity-Lock"); + } + + LocalDateTime now = LocalDateTime.now(); + CardLockEntity newLock = new CardLockEntity(); + newLock.setName(template.getName()); + newLock.setLockee(lockeeUserId); + newLock.setKeyholder(keyholderUserId); + newLock.setInitialCards(template.getInitialCards()); + newLock.setPickEveryMinute(template.getPickEveryMinute()); + newLock.setAccumulatePicks(template.isAccumulatePicks()); + newLock.setShowRemainingCards(template.isShowRemainingCards()); + newLock.setLatestOpeningtime(template.getLatestOpeningtime()); + newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); + newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); + newLock.setTasks(template.getTasks()); + newLock.setRequiresVerification(template.isRequiresVerification()); + newLock.setTestLock(false); + newLock.setTaskCardMode(template.getTaskCardMode()); + + int codeLines = template.getUnlockCodeLines() != null ? template.getUnlockCodeLines() : 5; + newLock.setUnlockCodeLines(codeLines); + StringBuilder codeBuilder = new StringBuilder(); + java.util.Random rng = new java.util.Random(); + for (int i = 0; i < codeLines; i++) codeBuilder.append(rng.nextInt(10)); + newLock.setUnlockCode(codeBuilder.toString()); + + newLock.setStartTime(now); + newLock.setAvailableCards(template.getInitialCards() != null + ? new ArrayList<>(template.getInitialCards()) : new ArrayList<>()); + newLock.setOpenPicks(0); + if (template.getPickEveryMinute() != null) { + newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute())); + } + if (template.getHygineOpeningEveryMinites() != null) { + newLock.setLastHygineOpening(now); + } + cardlockRepository.save(newLock); + + // Lockee benachrichtigen + if (lockeeUserId != null) { + userRepository.findById(keyholderUserId).ifPresent(keyholder -> + systemMessageService.send(keyholderUserId, lockeeUserId, + keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.", + "/activelock.html", MessageCause.GAME_STATE)); + } + + // Spielabschluss-Logik (History + XP + Cleanup) + LocalDateTime endTime = LocalDateTime.now(); + long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes(); + GameHistoryEntity entry = new GameHistoryEntity(); + entry.setGameName("BDSM Game"); + entry.setGameType(GameType.BDSM); + entry.setStartTime(entity.getStartZeit()); + entry.setEndTime(endTime); + entry.setDurationMinutes(durationMinutes); + entry.addParticipant(entity.getUserId(), GameRole.PLAYER); + entity.getMitspieler().stream() + .filter(m -> m.getUserId() != null) + .forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER)); + gameHistoryRepository.save(entry); + + int xp = (int) durationMinutes; + userRepository.findById(entity.getUserId()).ifPresent(u -> { + u.setBdsmXp(u.getBdsmXp() + xp); + userRepository.save(u); + }); + entity.getMitspieler().stream() + .filter(m -> m.getUserId() != null) + .forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { + u.setBdsmXp(u.getBdsmXp() + xp); + userRepository.save(u); + })); + + bereinige(entity); + + LOGGER.info("BDSM-Session {} in Chastity-Lock {} überführt (Lockee: {}, Keyholder: {})", + sessionId, newLock.getLockId(), lockeeUserId, keyholderUserId); + return newLock; + } + + /** Löscht alle Session-Daten (Sperren, Mitspieler, Session selbst). */ + private void bereinige(BdsmGameEntity entity) { + aktiveSperreRepository.deleteAll(entity.getAktiveSperren()); + mitspielerRepository.deleteAll(entity.getMitspieler()); + sessionRepository.delete(entity); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmEinladungController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmEinladungController.java index 5712ec1..c9dad1a 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmEinladungController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmEinladungController.java @@ -67,6 +67,8 @@ public class BdsmEinladungController { return ResponseEntity.status(403).build(); } + if (req.setupId() == null) return ResponseEntity.badRequest().build(); + // Prüfen ob Person bereits aktiv eingeladen oder Teil des Spiels boolean alreadyInvited = einladungRepository.findBySetupId(req.setupId()).stream() .anyMatch(e -> req.inviteeId().equals(e.getInviteeId()) @@ -113,6 +115,10 @@ public class BdsmEinladungController { if (e == null) return ResponseEntity.notFound().build(); if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build(); e.setStatus(Status.CANCELLED); + String inviterName = userRepository.findById(userId).map(u -> u.getName()).orElse("Jemand"); + systemMessageService.send(userId, e.getInviteeId(), + inviterName + " hat die BDSM-Spieleinladung zurückgezogen.", + "/einladungen.html", MessageCause.INVITATION); return ResponseEntity.accepted().build(); } @@ -168,7 +174,7 @@ public class BdsmEinladungController { } @GetMapping("/{id}") - public ResponseEntity> getById(@PathVariable UUID id, Principal principal) { + public ResponseEntity> getById(@PathVariable("id") UUID id, Principal principal) { UUID userId = currentUserId(principal); if (userId == null) return ResponseEntity.status(401).build(); BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java index 0457680..f21470f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java @@ -1,34 +1,17 @@ package de.oaa.xxx.games.bdsm.controller; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.oaa.xxx.games.bdsm.AufgabeAnzeige; -import de.oaa.xxx.games.bdsm.Mitspieler; -import de.oaa.xxx.games.bdsm.GeschlechtEnum; -import de.oaa.xxx.games.bdsm.Werkzeug; -import de.oaa.xxx.games.bdsm.BdsmGame; -import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren; -import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList; -import de.oaa.xxx.games.bdsm.sperre.SperreCallback; -import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback; -import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten; -import de.oaa.xxx.games.bdsm.entity.MitspielerEntity; -import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity; -import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity; -import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity; -import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository; -import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository; -import de.oaa.xxx.games.bdsm.repository.MitspielerRepository; -import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository; -import de.oaa.xxx.games.chastity.cardlock.CardLockEntity; -import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; -import de.oaa.xxx.games.history.GameHistoryEntity; -import de.oaa.xxx.games.history.GameHistoryRepository; -import de.oaa.xxx.games.history.GameRole; -import de.oaa.xxx.games.history.GameType; -import de.oaa.xxx.social.SystemMessageService; -import de.oaa.xxx.social.entity.MessageCause; -import de.oaa.xxx.user.UserRepository; +import java.security.Principal; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -45,52 +28,71 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.security.Principal; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.oaa.xxx.games.bdsm.AufgabeAnzeige; +import de.oaa.xxx.games.bdsm.BdsmGame; +import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren; +import de.oaa.xxx.games.bdsm.BdsmGameService; +import de.oaa.xxx.games.bdsm.GeschlechtEnum; +import de.oaa.xxx.games.bdsm.Mitspieler; +import de.oaa.xxx.games.bdsm.Werkzeug; +import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList; +import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity; +import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity; +import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity; +import de.oaa.xxx.games.bdsm.entity.MitspielerEntity; +import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository; +import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository; +import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository; +import de.oaa.xxx.games.bdsm.repository.MitspielerRepository; +import de.oaa.xxx.games.bdsm.sperre.SperreCallback; +import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten; +import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback; +import de.oaa.xxx.games.chastity.cardlock.CardLockEntity; +import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; +import de.oaa.xxx.social.SystemMessageService; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.user.UserRepository; @RestController @RequestMapping("/bdsm") @Transactional public class BdsmGameController { - private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class); - /** Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen beendet wurden. */ - private static final Set ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>()); + private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class); + /** + * Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen + * beendet wurden. + */ + private static final Set ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>()); - private final BdsmGameRepository sessionRepository; - private final MitspielerRepository mitspielerRepository; - private final AktiveSperreRepository aktiveSperreRepository; - private final UserRepository userRepository; - private final GameHistoryRepository gameHistoryRepository; - private final BdsmEinladungRepository einladungRepository; - private final ObjectMapper objectMapper; - private final SystemMessageService systemMessageService; - private final CardlockRepository cardlockRepository; + private final BdsmGameRepository sessionRepository; + private final MitspielerRepository mitspielerRepository; + private final AktiveSperreRepository aktiveSperreRepository; + private final UserRepository userRepository; + private final BdsmEinladungRepository einladungRepository; + private final ObjectMapper objectMapper; + private final SystemMessageService systemMessageService; + private final CardlockRepository cardlockRepository; + private final BdsmGameService bdsmGameService; - public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository, - AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository, - GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository, - ObjectMapper objectMapper, SystemMessageService systemMessageService, - CardlockRepository cardlockRepository) { - this.sessionRepository = sessionRepository; - this.mitspielerRepository = mitspielerRepository; - this.aktiveSperreRepository = aktiveSperreRepository; - this.userRepository = userRepository; - this.gameHistoryRepository = gameHistoryRepository; - this.einladungRepository = einladungRepository; - this.objectMapper = objectMapper; - this.systemMessageService = systemMessageService; - this.cardlockRepository = cardlockRepository; - } + public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository, + AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository, + BdsmEinladungRepository einladungRepository, ObjectMapper objectMapper, + SystemMessageService systemMessageService, CardlockRepository cardlockRepository, + BdsmGameService bdsmGameService) { + this.sessionRepository = sessionRepository; + this.mitspielerRepository = mitspielerRepository; + this.aktiveSperreRepository = aktiveSperreRepository; + this.userRepository = userRepository; + this.einladungRepository = einladungRepository; + this.objectMapper = objectMapper; + this.systemMessageService = systemMessageService; + this.cardlockRepository = cardlockRepository; + this.bdsmGameService = bdsmGameService; + } @GetMapping("/{sessionId}") public ResponseEntity getBySessionId(@PathVariable UUID sessionId) { @@ -159,46 +161,8 @@ public class BdsmGameController { public ResponseEntity spielAbgeschlossen(@PathVariable UUID sessionId) { BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null); if (entity == null) return ResponseEntity.notFound().build(); - - LocalDateTime endTime = LocalDateTime.now(); - long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes(); - - GameHistoryEntity entry = new GameHistoryEntity(); - entry.setGameName("BDSM Game"); - entry.setGameType(GameType.BDSM); - entry.setStartTime(entity.getStartZeit()); - entry.setEndTime(endTime); - entry.setDurationMinutes(durationMinutes); - entry.addParticipant(entity.getUserId(), GameRole.PLAYER); - entity.getMitspieler().stream() - .filter(m -> m.getUserId() != null) - .forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER)); - gameHistoryRepository.save(entry); - - // BDSM-XP für alle Teilnehmer gutschreiben (Minuten = XP) - int xp = (int) durationMinutes; - userRepository.findById(entity.getUserId()).ifPresent(u -> { - u.setBdsmXp(u.getBdsmXp() + xp); - userRepository.save(u); - }); - entity.getMitspieler().stream() - .filter(m -> m.getUserId() != null) - .forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { - u.setBdsmXp(u.getBdsmXp() + xp); - userRepository.save(u); - })); - - // Eigene-Gerät-Gäste über ordentliches Spielende benachrichtigen ORDENTLICH_BEENDET.add(sessionId); - String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉"; - entity.getMitspieler().stream() - .filter(m -> m.isEigenesGeraet() && m.getUserId() != null) - .forEach(m -> systemMessageService.send(entity.getUserId(), m.getUserId(), - endNachricht, "/userhome.html", MessageCause.GAME_STATE)); - - aktiveSperreRepository.deleteAll(entity.getAktiveSperren()); - mitspielerRepository.deleteAll(entity.getMitspieler()); - sessionRepository.delete(entity); + bdsmGameService.spielAbschliessen(entity); return ResponseEntity.accepted().build(); } @@ -547,89 +511,20 @@ public class BdsmGameController { @PostMapping("/{sessionId}/zu-chastity") public ResponseEntity> zuChastity( @PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) { - BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null); - if (entity == null) return ResponseEntity.notFound().build(); - - CardLockEntity template = cardlockRepository.findById(req.lockId()).orElse(null); - if (template == null) return ResponseEntity.badRequest().build(); - - // Lockee darf kein aktives Chastity-Lock haben - if (req.lockeeUserId() != null - && cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(req.lockeeUserId())) { + try { + CardLockEntity newLock = bdsmGameService.zuChastity( + sessionId, req.lockId(), req.lockeeUserId(), req.keyholderUserId()); + Map response = new LinkedHashMap<>(); + response.put("lockId", newLock.getLockId().toString()); + response.put("unlockCode", newLock.getUnlockCode()); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + String msg = e.getMessage(); + if (msg != null && msg.contains("Session")) return ResponseEntity.notFound().build(); + return ResponseEntity.badRequest().build(); + } catch (IllegalStateException e) { return ResponseEntity.status(409).build(); } - - // Neues Lock mit Template-Einstellungen für den BDSM-Lockee erstellen - LocalDateTime now = LocalDateTime.now(); - CardLockEntity newLock = new CardLockEntity(); - newLock.setName(template.getName()); - newLock.setLockee(req.lockeeUserId()); - newLock.setKeyholder(req.keyholderUserId()); - newLock.setInitialCards(template.getInitialCards()); - newLock.setPickEveryMinute(template.getPickEveryMinute()); - newLock.setAccumulatePicks(template.isAccumulatePicks()); - newLock.setShowRemainingCards(template.isShowRemainingCards()); - newLock.setLatestOpeningtime(template.getLatestOpeningtime()); - newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); - newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); - newLock.setTasks(template.getTasks()); - newLock.setRequiresVerification(template.isRequiresVerification()); - newLock.setTestLock(false); - newLock.setTaskCardMode(template.getTaskCardMode()); - int codeLines = template.getUnlockCodeLines() != null ? template.getUnlockCodeLines() : 5; - newLock.setUnlockCodeLines(codeLines); - StringBuilder codeBuilder = new StringBuilder(); - java.util.Random rng = new java.util.Random(); - for (int i = 0; i < codeLines; i++) codeBuilder.append(rng.nextInt(10)); - newLock.setUnlockCode(codeBuilder.toString()); - newLock.setStartTime(now); - newLock.setAvailableCards(template.getInitialCards() != null - ? new java.util.ArrayList<>(template.getInitialCards()) : new java.util.ArrayList<>()); - newLock.setOpenPicks(0); - if (template.getPickEveryMinute() != null) { - newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute())); - } - if (template.getHygineOpeningEveryMinites() != null) { - newLock.setLastHygineOpening(now); - } - cardlockRepository.save(newLock); - - // Lockee benachrichtigen (falls UserAccount vorhanden) - if (req.lockeeUserId() != null) { - userRepository.findById(req.keyholderUserId()).ifPresent(keyholder -> - systemMessageService.send(req.keyholderUserId(), req.lockeeUserId(), - keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.", - "/activelock.html", MessageCause.GAME_STATE)); - } - - // Spielabschluss-Logik (wie spielAbgeschlossen, aber ohne eigenen Delete) - LocalDateTime endTime = LocalDateTime.now(); - long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes(); - GameHistoryEntity entry = new GameHistoryEntity(); - entry.setGameName("BDSM Game"); - entry.setGameType(GameType.BDSM); - entry.setStartTime(entity.getStartZeit()); - entry.setEndTime(endTime); - entry.setDurationMinutes(durationMinutes); - entry.addParticipant(entity.getUserId(), GameRole.PLAYER); - entity.getMitspieler().stream() - .filter(m -> m.getUserId() != null) - .forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER)); - gameHistoryRepository.save(entry); - - int xp = (int) durationMinutes; - userRepository.findById(entity.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); }); - entity.getMitspieler().stream().filter(m -> m.getUserId() != null) - .forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); })); - - aktiveSperreRepository.deleteAll(entity.getAktiveSperren()); - mitspielerRepository.deleteAll(entity.getMitspieler()); - sessionRepository.delete(entity); - - Map response = new LinkedHashMap<>(); - response.put("lockId", newLock.getLockId().toString()); - response.put("unlockCode", newLock.getUnlockCode()); - return ResponseEntity.ok(response); } /** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */ diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmSetupDraftController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmSetupDraftController.java index 048f2cb..bfdec92 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmSetupDraftController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmSetupDraftController.java @@ -33,10 +33,15 @@ public class BdsmSetupDraftController { } @GetMapping - public ResponseEntity> getDraft(Principal principal) { + public ResponseEntity> getDraft( + @RequestParam(required = false) String setupId, + Principal principal) { UUID userId = currentUserId(principal); if (userId == null) return ResponseEntity.status(401).build(); - return draftRepository.findByUserId(userId) + var lookup = (setupId != null && !setupId.isBlank()) + ? draftRepository.findBySetupId(setupId) + : draftRepository.findByUserId(userId); + return lookup .map(d -> { Map m = new LinkedHashMap<>(); m.put("setupId", d.getSetupId()); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/repository/BdsmSetupDraftRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/repository/BdsmSetupDraftRepository.java index b4a587c..7b0ca40 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/repository/BdsmSetupDraftRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/bdsm/repository/BdsmSetupDraftRepository.java @@ -8,4 +8,5 @@ import java.util.UUID; public interface BdsmSetupDraftRepository extends JpaRepository { Optional findByUserId(UUID userId); + Optional findBySetupId(String setupId); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java index 72a49f1..b81d6d8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java @@ -41,7 +41,6 @@ import de.oaa.xxx.games.chastity.KeyholderInvitationEntity; import de.oaa.xxx.games.chastity.KeyholderInvitationRepository; import de.oaa.xxx.games.chastity.LockeeInvitationEntity; import de.oaa.xxx.games.chastity.LockeeInvitationRepository; -import de.oaa.xxx.games.history.GameHistoryRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.Task; @@ -56,1503 +55,1561 @@ import de.oaa.xxx.user.UserRepository; @RequestMapping("/keyholder") public class CardLockController { - private final CardlockRepository cardlockRepository; - private final CardLockRepository cardLockRepository; - private final UserRepository userRepository; - private final KeyholderInvitationRepository invitationRepository; - private final VerificationRepository verificationRepository; - private final VerificationVoteRepository verificationVoteRepository; - private final HygieneViolationRepository hygieneViolationRepository; - private final LockeeInvitationRepository lockeeInvitationRepository; - private final AssignedTaskRepository assignedTaskRepository; - private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; - private final CommunityTaskVoteRepository communityTaskVoteRepository; - private final UnlockCodeHistoryRepository unlockCodeHistoryRepository; - private final UnlockCodeHistoryService unlockCodeHistoryService; - private final GameHistoryRepository gameHistoryRepository; - private final SystemMessageService systemMessageService; - - @Value("${app.base-url:http://localhost:8080}") - private String baseUrl; - - public CardLockController(CardlockRepository cardlockRepository, - CardLockRepository cardLockRepository, - UserRepository userRepository, - KeyholderInvitationRepository invitationRepository, - VerificationRepository verificationRepository, - VerificationVoteRepository verificationVoteRepository, - HygieneViolationRepository hygieneViolationRepository, - LockeeInvitationRepository lockeeInvitationRepository, - AssignedTaskRepository assignedTaskRepository, - KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, - CommunityTaskVoteRepository communityTaskVoteRepository, - UnlockCodeHistoryRepository unlockCodeHistoryRepository, - UnlockCodeHistoryService unlockCodeHistoryService, - GameHistoryRepository gameHistoryRepository, - SystemMessageService systemMessageService) { - this.cardlockRepository = cardlockRepository; - this.cardLockRepository = cardLockRepository; - this.userRepository = userRepository; - this.invitationRepository = invitationRepository; - this.verificationRepository = verificationRepository; - this.verificationVoteRepository = verificationVoteRepository; - this.hygieneViolationRepository = hygieneViolationRepository; - this.lockeeInvitationRepository = lockeeInvitationRepository; - this.assignedTaskRepository = assignedTaskRepository; - this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; - this.communityTaskVoteRepository = communityTaskVoteRepository; - this.unlockCodeHistoryRepository = unlockCodeHistoryRepository; - this.unlockCodeHistoryService = unlockCodeHistoryService; - this.gameHistoryRepository = gameHistoryRepository; - this.systemMessageService = systemMessageService; - } - - record CreateCardLockRequest( - String name, - UUID keyholder, - UUID lockeeUserId, - boolean lockeeDetailsVisible, - List initialCards, - Integer pickEveryMinute, - boolean accumulatePicks, - boolean showRemainingCards, - LocalDateTime latestOpeningtime, - Integer hygineOpeningDurationMinutes, - Integer hygineOpeningEveryMinites, - List tasks, - boolean requiresVerification, - boolean testLock, - Integer unlockCodeLines, - String taskCardMode - ) {} - - private static final SecureRandom RNG = new SecureRandom(); - - private String generateUnlockCode(int lines) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < lines; i++) { - sb.append(RNG.nextInt(10)); - } - return sb.toString(); - } - - @PostMapping("/cardlock") - public ResponseEntity> createCardLock( - @RequestBody CreateCardLockRequest req, - Principal principal) { - - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - if (req.initialCards() == null || req.initialCards().isEmpty() - || req.pickEveryMinute() == null || req.pickEveryMinute() < 1) - return ResponseEntity.badRequest().build(); - - // Friend-lockee path: current user becomes keyholder, invite lockee - boolean friendLockee = req.lockeeUserId() != null && !req.lockeeUserId().equals(myId); - if (friendLockee) { - var lockeeOpt = userRepository.findById(req.lockeeUserId()); - if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build(); - var lockee = lockeeOpt.get(); - - LocalDateTime now = LocalDateTime.now(); - CardLockEntity lock = new CardLockEntity(); - lock.setName(req.name()); - lock.setLockee(lockee.getUserId()); - lock.setKeyholder(myId); - lock.setInitialCards(req.initialCards()); - lock.setPickEveryMinute(req.pickEveryMinute()); - lock.setAccumulatePicks(req.accumulatePicks()); - lock.setShowRemainingCards(req.showRemainingCards()); - lock.setLatestOpeningtime(req.latestOpeningtime()); - lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); - lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); - lock.setRequiresVerification(req.requiresVerification()); - lock.setTestLock(false); - lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM"); - // startTime, unlockCode, unlockCodeLines left null until lockee accepts - cardlockRepository.save(lock); - - String token = UUID.randomUUID().toString().replace("-", ""); - LockeeInvitationEntity inv = new LockeeInvitationEntity(); - inv.setLockId(lock.getLockId()); - inv.setLockeeUserId(lockee.getUserId()); - inv.setKeyholderUserId(myId); - inv.setToken(token); - inv.setCreatedAt(now); - inv.setDetailsVisible(req.lockeeDetailsVisible()); - lockeeInvitationRepository.save(inv); - - String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; - sendMessage(myId, lockee.getUserId(), - me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.", - "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION); - - return ResponseEntity.ok(Map.of( - "lockId", lock.getLockId().toString(), - "lockeeInvitationSent", true - )); - } - - // Self-lockee path (existing behavior) - if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) - return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); - - int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) - ? req.unlockCodeLines() : 5; - String unlockCode = generateUnlockCode(codeLines); - - CardLockEntity lock = new CardLockEntity(); - lock.setName(req.name()); - lock.setLockee(myId); - lock.setKeyholder(null); // set only after invitation is confirmed - lock.setInitialCards(req.initialCards()); - lock.setPickEveryMinute(req.pickEveryMinute()); - lock.setAccumulatePicks(req.accumulatePicks()); - lock.setShowRemainingCards(req.showRemainingCards()); - lock.setLatestOpeningtime(req.latestOpeningtime()); - lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); - lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); - lock.setRequiresVerification(req.requiresVerification()); - lock.setTestLock(req.testLock()); - lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM"); - lock.setUnlockCodeLines(codeLines); - lock.setUnlockCode(unlockCode); - - LocalDateTime now = LocalDateTime.now(); - lock.setStartTime(now); - lock.setAvailableCards(new ArrayList<>(req.initialCards())); - lock.setOpenPicks(0); - lock.setNextCardIn(now.plusMinutes(req.pickEveryMinute())); - if (req.hygineOpeningEveryMinites() != null) { - lock.setLastHygineOpening(now); - } - - cardlockRepository.save(lock); - - boolean keyholderPending = false; - if (req.keyholder() != null) { - var khOpt = userRepository.findById(req.keyholder()); - if (khOpt.isPresent()) { - var kh = khOpt.get(); - String token = UUID.randomUUID().toString().replace("-", ""); - - KeyholderInvitationEntity inv = new KeyholderInvitationEntity(); - inv.setLockId(lock.getLockId()); - inv.setKeyholderUserId(kh.getUserId()); - inv.setLockeeUserId(myId); - inv.setToken(token); - inv.setCreatedAt(now); - invitationRepository.save(inv); - - String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; - sendMessage(me.getUserId(), kh.getUserId(), - me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.", - "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION); - - keyholderPending = true; - } - } - - return ResponseEntity.ok(Map.of( - "lockId", lock.getLockId().toString(), - "unlockCode", unlockCode, - "keyholderPending", keyholderPending - )); - } - - @PostMapping("/cardlock/{lockId}/draw") - @Transactional - public ResponseEntity> drawCard(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository); - CardDTO dto = service.getNextCard(); - if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar")); - - // Task-Karte in nicht-zufälligem Modus → Entscheidung delegieren - String taskPending = null; - if (dto.card() == CardEnum.TASK && !"RANDOM".equals(l.getTaskCardMode()) - && l.getTasks() != null && !l.getTasks().isEmpty()) { - - if ("KEYHOLDER".equals(l.getTaskCardMode()) && l.getKeyholder() != null) { - KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity(); - choice.setLockId(l.getLockId()); - choice.setCreatedAt(LocalDateTime.now()); - choice.setStatus("PENDING"); - keyholderTaskChoiceRepository.save(choice); - userRepository.findById(l.getKeyholder()).ifPresent(kh -> - sendMessage(l.getLockee(), kh.getUserId(), - "Deine Lockee hat eine Aufgaben-Karte gezogen – wähle eine Aufgabe aus.", - "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE)); - taskPending = "KEYHOLDER"; - - } else if ("COMMUNITY".equals(l.getTaskCardMode())) { - CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity(); - vote.setLockId(l.getLockId()); - vote.setCreatedAt(LocalDateTime.now()); - vote.setExpiresAt(LocalDateTime.now().plusHours(1)); - vote.setStatus("ACTIVE"); - vote.setTestLock(l.isTestLock()); - communityTaskVoteRepository.save(vote); - taskPending = l.isTestLock() ? "RANDOM" : "COMMUNITY"; - } - } - - Map result = new HashMap<>(); - result.put("card", dto.card().name()); - result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : ""); - if (taskPending != null) result.put("taskPending", taskPending); - - // Grüne Karte → Entsperrcode-Historie speichern + Keyholder benachrichtigen - if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) { - unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD"); - if (l.getKeyholder() != null) { - sendMessage(myId, l.getKeyholder(), - meOpt.get().getName() + " hat die grüne Karte gezogen! Der Entsperrcode wurde angezeigt.", - "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - } - } - - return ResponseEntity.ok(result); - } - - @PostMapping("/cardlock/{lockId}/hygiene/start") - @Transactional - public ResponseEntity> startHygieneOpening(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - l.setHygineOpeningtime(LocalDateTime.now()); - cardlockRepository.save(l); - - unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "HYGIENE_OPEN"); - - return ResponseEntity.ok(Map.of( - "unlockCode", l.getUnlockCode(), - "durationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 30 - )); - } - - @PostMapping("/cardlock/{lockId}/hygiene/end") - @Transactional - public ResponseEntity> endHygieneOpening(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - LocalDateTime now = LocalDateTime.now(); - - // Overtime berechnen - if (l.getHygineOpeningtime() != null && l.getHygineOpeningDurationMinutes() != null) { - LocalDateTime dueTime = l.getHygineOpeningtime().plusMinutes(l.getHygineOpeningDurationMinutes()); - if (now.isAfter(dueTime)) { - long overtimeMinutes = ChronoUnit.MINUTES.between(dueTime, now); - if (l.getKeyholder() == null) { - // Self-Lock: 4-fache Überschreitungszeit einfrieren - if (l.getFrozenUntill() != null) { - l.setFrozenUntill(l.getFrozenUntill().plusMinutes(overtimeMinutes * 4)); - } else { - l.setFrozenUntill(now.plusMinutes(overtimeMinutes * 4)); - } - } else { - // Keyholder vorhanden: Verletzung protokollieren - HygieneViolationEntity violation = new HygieneViolationEntity(); - violation.setLockId(lockId); - violation.setLockeeId(myId); - violation.setKeyholderUserId(l.getKeyholder()); - violation.setViolationTime(now); - violation.setOvertimeMinutes(overtimeMinutes); - hygieneViolationRepository.save(violation); - } - } - } - - // Nächsten Öffnungszeitpunkt setzen - l.setLastHygineOpening(LocalDateTime.now()); - l.setHygineOpeningtime(null); - - // Neuen Entsperrcode generieren - int codeLines = l.getUnlockCodeLines() != null ? l.getUnlockCodeLines() : 5; - String newCode = generateUnlockCode(codeLines); - l.setUnlockCode(newCode); - cardlockRepository.save(l); - - unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), newCode, "HYGIENE_CLOSE"); - - return ResponseEntity.ok(Map.of("newUnlockCode", newCode)); - } - - @PostMapping("/cardlock/{lockId}/task/complete") - @Transactional - public ResponseEntity completeTask(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository); - service.clearTask(); - return ResponseEntity.noContent().build(); - } - - @PostMapping("/cardlock/{lockId}/green/keep") - @Transactional - public ResponseEntity greenKeep(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository); - service.putBackGreen(); - - // Grüne Karte zurückgelegt → Keyholder benachrichtigen - if (l.getKeyholder() != null) { - sendMessage(myId, l.getKeyholder(), - meOpt.get().getName() + " hat die grüne Karte zurückgelegt und bleibt im Lock.", - "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - } - - return ResponseEntity.noContent().build(); - } - - @GetMapping("/mylock") - public ResponseEntity> getMyActiveLock(Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var locks = cardlockRepository.findByLockee(meOpt.get().getUserId()); - var active = locks.stream() - .filter(l -> l.getUnlockTime() == null) - .findFirst(); - if (active.isEmpty()) return ResponseEntity.noContent().build(); - return ResponseEntity.ok(Map.of("lockId", active.get().getLockId().toString())); - } - - @GetMapping("/cardlock/{lockId}") - public ResponseEntity> getLock(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - Map cardCounts = new LinkedHashMap<>(); - if (l.getAvailableCards() != null) { - l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); - } - long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0; - - // Hygiene-Berechnung - boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; - boolean hygieneOpeningDue = false; - long hygieneSecondsRemaining = 0; - if (hygieneEnabled) { - LocalDateTime base = l.getLastHygineOpening() != null - ? l.getLastHygineOpening() - : l.getStartTime(); - if (base != null) { - LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); - hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); - hygieneOpeningDue = hygieneSecondsRemaining <= 0; - } - } - - Map result = new HashMap<>(); - result.put("lockId", l.getLockId().toString()); - result.put("name", l.getName() != null ? l.getName() : ""); - result.put("showRemainingCards", l.isShowRemainingCards()); - result.put("availableCardCounts", cardCounts); - result.put("totalCards", totalCards); - result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); - result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : ""); - result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); - result.put("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null); - result.put("currentTaskDescription", l.getCurrentTaskDescription()); - result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null); - result.put("hygieneEnabled", hygieneEnabled); - result.put("hygieneOpeningDue", hygieneOpeningDue); - result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); - result.put("hygieneOpeningActive", l.getHygineOpeningtime() != null); - result.put("hygieneOpeningStarted", l.getHygineOpeningtime() != null ? l.getHygineOpeningtime().toString() : null); - result.put("hygieneDurationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0); - result.put("hasKeyholder", l.getKeyholder() != null); - result.put("keyholderInvitationPending", l.getKeyholder() == null && - !invitationRepository.findByLockId(l.getLockId()).isEmpty()); - if (l.getKeyholder() != null) { - userRepository.findById(l.getKeyholder()).ifPresent(kh -> { - result.put("keyholderName", kh.getName()); - result.put("keyholderUserId", kh.getUserId().toString()); - result.put("keyholderProfilePic", kh.getProfilePicture()); - }); - } - - // Verifikation - boolean verificationDue = false; - String verificationTodayId = null; - String verificationPendingId = null; - String verificationPendingCode = null; - long verificationUpvotes = 0; - long verificationDownvotes = 0; - if (l.isRequiresVerification()) { - LocalDateTime todayStart = LocalDate.now().atStartOfDay(); - LocalDateTime todayEnd = todayStart.plusDays(1); - var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd); - if (!completed.isEmpty()) { - var todayV = completed.get(0); - verificationTodayId = todayV.getVerficationId().toString(); - var votes = verificationVoteRepository.findAllByVerificationId(todayV.getVerficationId()); - verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); - verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); - } else { - verificationDue = true; - var pending = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(l.getLockId(), todayStart, todayEnd); - if (!pending.isEmpty()) { - verificationPendingId = pending.get(0).getVerficationId().toString(); - verificationPendingCode = pending.get(0).getCode(); - } - } - } - result.put("verificationRequired", l.isRequiresVerification()); - result.put("verificationDue", verificationDue); - result.put("verificationTodayId", verificationTodayId); - result.put("verificationUpvotes", verificationUpvotes); - result.put("verificationDownvotes", verificationDownvotes); - result.put("verificationPendingId", verificationPendingId); - result.put("verificationPendingCode", verificationPendingCode); - - // Abgelaufene Aufgaben prüfen und Strafe anwenden - boolean lockDirty = false; - var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING") - .stream().filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList(); - for (var t : expiredTasks) { - t.setStatus("EXPIRED"); - applyAssignedTaskPenalty(l, t); - assignedTaskRepository.save(t); - lockDirty = true; - sendMessage(l.getKeyholder(), l.getLockee(), - "Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.", - "/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - } - if (lockDirty) cardlockRepository.save(l); - - // Ausstehende Keyholder-Aufgaben (ohne Aufgabentext) - var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING") - .stream() - .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())) - .map(t -> { - Map m = new LinkedHashMap<>(); - m.put("taskId", t.getTaskId().toString()); - m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText()); - m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : ""); - m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0); - m.put("assignedAt", t.getAssignedAt().toString()); - m.put("acceptDeadline", t.getAcceptDeadline().toString()); - m.put("penaltyFreezeMinutes", t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0); - m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0); - return m; - }) - .toList(); - result.put("assignedTasks", pendingAssigned); - result.put("taskCardMode", l.getTaskCardMode()); - - // Ausstehende Keyholder-Choices - boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository - .findByLockIdAndStatus(l.getLockId(), "PENDING").isEmpty(); - result.put("pendingKeyholderChoice", pendingKeyholderChoice); - - // Aktive Community-Vote - var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE").stream() - .filter(v -> v.getLockId().equals(l.getLockId())).findFirst(); - if (activeVotes.isPresent()) { - var v = activeVotes.get(); - result.put("activeCommunityVote", Map.of( - "voteSessionId", v.getVoteSessionId().toString(), - "expiresAt", v.getExpiresAt().toString() - )); - } - - // Notfall-Entsperrung: nach 1 Stunde automatisch öffnen - if (l.getEmergencyUnlockRequestedAt() != null - && !l.isKeyholderRequestedUnlock() - && l.getEmergencyUnlockRequestedAt().isBefore(LocalDateTime.now().minusHours(1))) { - l.setEmergencyAutoUnlocked(true); - l.setKeyholderRequestedUnlock(true); - cardlockRepository.save(l); - } - - // Keyholder hat Unlock angefordert → Unlock-Code mitliefern - result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock()); - if (l.isKeyholderRequestedUnlock()) { - result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); - // Notfall-Freigaben werden nicht in der Historie gespeichert - if (l.getEmergencyUnlockRequestedAt() == null) { - unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "KEYHOLDER_UNLOCK"); - } - } - - result.put("testLock", l.isTestLock()); - result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); - if (l.isTestLock()) { - result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); - } - - return ResponseEntity.ok(result); - } - - @PostMapping("/cardlock/{lockId}/verification/start") - @Transactional - public ResponseEntity> startVerification(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - LocalDateTime todayStart = LocalDate.now().atStartOfDay(); - LocalDateTime todayEnd = todayStart.plusDays(1); - - // Existierende Verifikation für heute zurückgeben statt neue anlegen - var existing = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(lockId, todayStart, todayEnd); - if (!existing.isEmpty()) { - var ev = existing.get(0); - return ResponseEntity.ok(Map.of("verificationId", ev.getVerficationId().toString(), "code", ev.getCode())); - } - var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, todayStart, todayEnd); - if (!completed.isEmpty()) { - var cv = completed.get(0); - return ResponseEntity.ok(Map.of("verificationId", cv.getVerficationId().toString(), "code", cv.getCode())); - } - - VerificationEntity v = new VerificationEntity(); - v.setVerficationId(UUID.randomUUID()); - v.setLockId(lockId); - v.setLockeeId(myId); - v.setCode(CodeCreator.createAlphanumericCode(6)); - v.setVerificationTime(LocalDateTime.now()); - if (l.getKeyholder() != null) v.setKeyholderId(l.getKeyholder()); - verificationRepository.save(v); - - return ResponseEntity.ok(Map.of( - "verificationId", v.getVerficationId().toString(), - "code", v.getCode() - )); - } - - @PostMapping(value = "/cardlock/{lockId}/verification/{verificationId}/complete", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @Transactional - public ResponseEntity completeVerification( - @PathVariable UUID lockId, - @PathVariable UUID verificationId, - @RequestParam MultipartFile image, - Principal principal) throws IOException { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - if (!lockOpt.get().getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - var vOpt = verificationRepository.findById(verificationId); - if (vOpt.isEmpty()) return ResponseEntity.notFound().build(); - var v = vOpt.get(); - if (!v.getLockId().equals(lockId)) return ResponseEntity.status(403).build(); - - v.setImage(scaleImage(image.getBytes(), 1024)); - verificationRepository.save(v); - - var lock = lockOpt.get(); - if (lock.getKeyholder() != null) { - var lockee = meOpt.get(); - sendMessage(myId, lock.getKeyholder(), - "📸 " + lockee.getName() + " hat eine Verifikation eingereicht.", - "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - } - - return ResponseEntity.noContent().build(); - } - - private byte[] scaleImage(byte[] input, int maxSize) throws IOException { - BufferedImage original = ImageIO.read(new ByteArrayInputStream(input)); - if (original == null) return input; - int w = original.getWidth(); - int h = original.getHeight(); - if (w <= maxSize && h <= maxSize) return input; - double scale = (double) maxSize / Math.max(w, h); - int newW = (int) (w * scale); - int newH = (int) (h * scale); - BufferedImage scaled = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB); - Graphics2D g = scaled.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(original, 0, 0, newW, newH, null); - g.dispose(); - String format = "jpeg"; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ImageIO.write(scaled, format, out); - return out.toByteArray(); - } - - @DeleteMapping("/cardlock/{lockId}/verification/today") - @Transactional - public ResponseEntity renewVerification(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - if (!lockOpt.get().getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - LocalDateTime todayStart = LocalDate.now().atStartOfDay(); - LocalDateTime todayEnd = todayStart.plusDays(1); - var completed = verificationRepository - .findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, todayStart, todayEnd); - for (var v : completed) { - verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId()); - verificationRepository.delete(v); - } - return ResponseEntity.noContent().build(); - } - - // ── Keyholder-Dashboard Endpunkte ── - - @GetMapping("/invitations/mine") - public ResponseEntity>> getMyInvitations(Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var invitations = invitationRepository.findByKeyholderUserId(myId); - List> result = new ArrayList<>(); - for (var inv : invitations) { - var lockOpt = cardlockRepository.findById(inv.getLockId()); - if (lockOpt.isEmpty()) continue; - var lock = lockOpt.get(); - if (lock.getKeyholder() != null) continue; // bereits akzeptiert - var lockeeOpt = userRepository.findById(lock.getLockee()); - if (lockeeOpt.isEmpty()) continue; - var lockee = lockeeOpt.get(); - Map item = new HashMap<>(); - item.put("lockId", inv.getLockId().toString()); - item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); - item.put("lockeeName", lockee.getName()); - item.put("lockeeId", lockee.getUserId().toString()); - item.put("lockeeProfilePic", lockee.getProfilePicture()); - item.put("token", inv.getToken()); - item.put("createdAt", inv.getCreatedAt().toString()); - result.add(item); - } - return ResponseEntity.ok(result); - } - - @Transactional - @DeleteMapping("/invitations/mine/{token}") - public ResponseEntity declineInvitation(@PathVariable String token, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var invOpt = invitationRepository.findByToken(token); - if (invOpt.isEmpty()) return ResponseEntity.notFound().build(); - var inv = invOpt.get(); - if (!inv.getKeyholderUserId().equals(myId)) return ResponseEntity.status(403).build(); - - var lockOpt = cardlockRepository.findById(inv.getLockId()); - invitationRepository.delete(inv); - - if (lockOpt.isPresent()) { - var lock = lockOpt.get(); - String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; - sendMessage(myId, lock.getLockee(), - me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.", - null, de.oaa.xxx.social.entity.MessageCause.INVITATION); - } - - return ResponseEntity.noContent().build(); - } - - @GetMapping("/invitations/sent") - public ResponseEntity>> getSentKeyholderInvitations(Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var invitations = invitationRepository.findByLockeeUserId(myId); - List> result = new ArrayList<>(); - for (var inv : invitations) { - var lockOpt = cardlockRepository.findById(inv.getLockId()); - if (lockOpt.isEmpty()) continue; - var lock = lockOpt.get(); - if (lock.getKeyholder() != null) continue; // already accepted - var khOpt = userRepository.findById(inv.getKeyholderUserId()); - if (khOpt.isEmpty()) continue; - var kh = khOpt.get(); - Map item = new HashMap<>(); - item.put("lockId", lock.getLockId().toString()); - item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); - item.put("keyholderName", kh.getName()); - item.put("keyholderProfilePic", kh.getProfilePicture()); - item.put("token", inv.getToken()); - item.put("createdAt", inv.getCreatedAt().toString()); - result.add(item); - } - return ResponseEntity.ok(result); - } - - @Transactional - @DeleteMapping("/invitations/sent/{token}") - public ResponseEntity cancelSentKeyholderInvitation(@PathVariable String token, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var invOpt = invitationRepository.findByToken(token); - if (invOpt.isEmpty()) return ResponseEntity.notFound().build(); - var inv = invOpt.get(); - - // Verify the lock belongs to the current user as lockee - var lockOpt = cardlockRepository.findById(inv.getLockId()); - if (lockOpt.isEmpty() || !lockOpt.get().getLockee().equals(myId)) - return ResponseEntity.status(403).build(); - - invitationRepository.delete(inv); - - String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank() - ? lockOpt.get().getName() : "Unbenanntes Lock"; - sendMessage(myId, inv.getKeyholderUserId(), - me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.", - null, de.oaa.xxx.social.entity.MessageCause.INVITATION); - - return ResponseEntity.noContent().build(); - } - - @GetMapping("/as-keyholder") - public ResponseEntity>> getLocksAsKeyholder(Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var locks = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(myId); - List> result = new ArrayList<>(); - for (var lock : locks) { - var lockeeOpt = userRepository.findById(lock.getLockee()); - if (lockeeOpt.isEmpty()) continue; - var lockee = lockeeOpt.get(); - Map item = new HashMap<>(); - item.put("lockId", lock.getLockId().toString()); - item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); - item.put("lockeeName", lockee.getName()); - item.put("lockeeId", lockee.getUserId().toString()); - item.put("lockeeProfilePic", lockee.getProfilePicture()); - item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0); - item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null); - boolean frozenByKh = lock.getFrozenUntill() != null - && lock.getFrozenUntill().isAfter(LocalDateTime.now()) - && (lock.getCurrentTask() == null || lock.getCurrentTask().isBlank()); - item.put("isFrozenByKeyholder", frozenByKh); - result.add(item); - } - return ResponseEntity.ok(result); - } - - @GetMapping("/as-keyholder/{lockId}") - public ResponseEntity> getLockAsKeyholder(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - - var lockeeOpt = userRepository.findById(l.getLockee()); - if (lockeeOpt.isEmpty()) return ResponseEntity.notFound().build(); - var lockee = lockeeOpt.get(); - - Map cardCounts = new LinkedHashMap<>(); - if (l.getAvailableCards() != null) { - l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); - } - - boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; - boolean hygieneOpeningDue = false; - long hygieneSecondsRemaining = 0; - if (hygieneEnabled) { - LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); - if (base != null) { - LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); - hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); - hygieneOpeningDue = hygieneSecondsRemaining <= 0; - } - } - - boolean verificationDue = false; - boolean verificationDoneToday = false; - String verificationMyVote = null; // null = not voted, "upvote", "downvote" - String verificationTodayId = null; - String verificationImage = null; - long verificationUpvotes = 0, verificationDownvotes = 0; - if (l.isRequiresVerification()) { - LocalDateTime todayStart = LocalDate.now().atStartOfDay(); - LocalDateTime todayEnd = todayStart.plusDays(1); - var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, todayStart, todayEnd); - if (!completed.isEmpty()) { - verificationDoneToday = true; - var v = completed.get(0); - var votes = verificationVoteRepository.findAllByVerificationId(v.getVerficationId()); - verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); - verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); - verificationTodayId = v.getVerficationId().toString(); - var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getVerficationId(), myId); - if (myVoteOpt.isPresent()) { - verificationMyVote = myVoteOpt.get().isUpvote() ? "upvote" : "downvote"; - } else if (v.getImage() != null) { - verificationImage = java.util.Base64.getEncoder().encodeToString(v.getImage()); - } - } else { - verificationDue = true; - } - } - - var recentViolations = hygieneViolationRepository.findByLockId(lockId).stream() - .sorted((a, b) -> b.getViolationTime().compareTo(a.getViolationTime())) - .limit(5) - .map(v -> Map.of("time", v.getViolationTime().toString(), "overtimeMinutes", v.getOvertimeMinutes())) - .toList(); - - Map result = new HashMap<>(); - result.put("lockId", l.getLockId().toString()); - result.put("lockName", l.getName() != null ? l.getName() : "Unbenanntes Lock"); - result.put("lockeeName", lockee.getName()); - result.put("lockeeId", lockee.getUserId().toString()); - result.put("lockeeProfilePic", lockee.getProfilePicture()); - result.put("totalCards", l.getAvailableCards() != null ? l.getAvailableCards().size() : 0); - result.put("cardCounts", cardCounts); - result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); - result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null); - result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); - result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null); - boolean isFrozenByKeyholder = l.getFrozenUntill() != null && l.getFrozenUntill().isAfter(LocalDateTime.now()); - result.put("isFrozenByKeyholder", isFrozenByKeyholder); - result.put("currentTask", l.getCurrentTask()); - result.put("currentTaskDescription", l.getCurrentTaskDescription()); - result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null); - result.put("hygieneEnabled", hygieneEnabled); - result.put("hygieneOpeningDue", hygieneOpeningDue); - result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); - result.put("hygieneOpeningActive", l.getHygineOpeningtime() != null); - result.put("requiresVerification", l.isRequiresVerification()); - result.put("verificationDue", verificationDue); - result.put("verificationDoneToday", verificationDoneToday); - result.put("verificationTodayId", verificationTodayId); - result.put("verificationMyVote", verificationMyVote); - result.put("verificationImage", verificationImage); - result.put("verificationUpvotes", verificationUpvotes); - result.put("verificationDownvotes", verificationDownvotes); - result.put("hygieneViolations", recentViolations); - result.put("hasTasks", l.getTasks() != null && !l.getTasks().isEmpty()); - if (l.getTasks() != null) { - var taskList = l.getTasks().stream() - .map(t -> { - Map m = new LinkedHashMap<>(); - m.put("title", t.resolveTitle()); - m.put("description", t.getDescription() != null ? t.getDescription() : ""); - m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); - return m; - }) - .toList(); - result.put("taskList", taskList); - } else { - result.put("taskList", List.of()); - } - var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING") - .stream() - .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())) - .map(t -> { - Map m = new LinkedHashMap<>(); - m.put("taskId", t.getTaskId().toString()); - m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText()); - m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : ""); - m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0); - m.put("assignedAt", t.getAssignedAt().toString()); - m.put("acceptDeadline", t.getAcceptDeadline().toString()); - m.put("penaltyFreezeMinutes", t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0); - m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0); - return m; - }) - .toList(); - result.put("pendingAssignedTasks", pendingAssigned); - result.put("taskCardMode", l.getTaskCardMode()); - - // Ausstehende Task-Karten-Choices (KEYHOLDER-Modus) - List lockTasks = l.getTasks() != null ? l.getTasks() : List.of(); - List> taskListForChoice = new ArrayList<>(); - for (int i = 0; i < lockTasks.size(); i++) { - Task t = lockTasks.get(i); - Map tm = new LinkedHashMap<>(); - tm.put("index", i); - tm.put("title", t.resolveTitle()); - tm.put("description", t.getDescription() != null ? t.getDescription() : ""); - tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); - taskListForChoice.add(tm); - } - var pendingChoices = keyholderTaskChoiceRepository.findByLockIdAndStatus(lockId, "PENDING") - .stream() - .map(c -> { - Map cm = new LinkedHashMap<>(); - cm.put("choiceId", c.getChoiceId().toString()); - cm.put("createdAt", c.getCreatedAt().toString()); - cm.put("tasks", taskListForChoice); - return cm; - }) - .toList(); - result.put("pendingTaskChoices", pendingChoices); - result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock()); - result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); - result.put("emergencyUnlockRequestedAt", l.getEmergencyUnlockRequestedAt() != null ? l.getEmergencyUnlockRequestedAt().toString() : null); - - return ResponseEntity.ok(result); - } - - @Transactional - @DeleteMapping("/cardlock/{lockId}") - public ResponseEntity deleteLock(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - // Entsperrung protokollieren (History + XP) – gültig nur wenn Keyholder vorhanden und kein Auto-Notfall - CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository); - service.unlock(l.getUnlockCode()); - - var verifications = verificationRepository.findByLockId(lockId); - verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId())); - verificationRepository.deleteAll(verifications); - invitationRepository.deleteByLockId(lockId); - cardlockRepository.deleteById(lockId); - return ResponseEntity.noContent().build(); - } - - record ModifyCardsRequest(Map cards, boolean notifyDetailed) {} - - @PostMapping("/as-keyholder/{lockId}/cards/add") - @Transactional - public ResponseEntity addCards(@PathVariable UUID lockId, - @RequestBody ModifyCardsRequest req, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - if (req.cards() == null || req.cards().isEmpty()) return ResponseEntity.badRequest().build(); - - List toAdd = new ArrayList<>(); - for (var entry : req.cards().entrySet()) { - try { - CardEnum type = CardEnum.valueOf(entry.getKey()); - int count = entry.getValue() != null ? Math.max(0, entry.getValue()) : 0; - for (int i = 0; i < count; i++) toAdd.add(type); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().build(); - } - } - if (toAdd.isEmpty()) return ResponseEntity.badRequest().build(); - - List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); - cards.addAll(toAdd); - l.setAvailableCards(cards); - cardlockRepository.save(l); - - String detail = toAdd.stream() - .collect(Collectors.groupingBy(c -> c, Collectors.counting())) - .entrySet().stream() - .map(e -> e.getValue() + "x " + cardLabel(e.getKey())) - .collect(Collectors.joining(", ")); - String msgText = req.notifyDetailed() - ? me.getName() + " hat " + toAdd.size() + " Karte(n) zu deinem Lock hinzugefügt: " + detail + "." - : me.getName() + " hat Karten zu deinem Lock hinzugefügt."; - - sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId, - de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - @PostMapping("/as-keyholder/{lockId}/cards/remove") - @Transactional - public ResponseEntity> removeCards(@PathVariable UUID lockId, - @RequestBody ModifyCardsRequest req, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - if (req.cards() == null || req.cards().isEmpty()) return ResponseEntity.badRequest().build(); - - List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); - - // Plausi: letzte grüne Karte darf nicht entfernt werden - long greenInDeck = cards.stream().filter(c -> c == CardEnum.GREEN).count(); - int greenToRemove = req.cards().getOrDefault("GREEN", 0); - if (greenInDeck > 0 && greenToRemove >= greenInDeck) { - return ResponseEntity.badRequest().body(Map.of("error", "Die letzte grüne Karte darf nicht entfernt werden.")); - } - - List removed = new ArrayList<>(); - for (var entry : req.cards().entrySet()) { - try { - CardEnum type = CardEnum.valueOf(entry.getKey()); - int count = entry.getValue() != null ? Math.max(0, entry.getValue()) : 0; - Iterator it = cards.iterator(); - int done = 0; - while (it.hasNext() && done < count) { - if (it.next() == type) { it.remove(); removed.add(type); done++; } - } - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().build(); - } - } - if (removed.isEmpty()) return ResponseEntity.badRequest().build(); - - l.setAvailableCards(cards); - cardlockRepository.save(l); - - String detail = removed.stream() - .collect(Collectors.groupingBy(c -> c, Collectors.counting())) - .entrySet().stream() - .map(e -> e.getValue() + "x " + cardLabel(e.getKey())) - .collect(Collectors.joining(", ")); - String msgText = req.notifyDetailed() - ? me.getName() + " hat " + removed.size() + " Karte(n) aus deinem Lock entfernt: " + detail + "." - : me.getName() + " hat Karten aus deinem Lock entfernt."; - - sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId, - de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - // ── Hilfsmethoden ────────────────────────────────────────────────────────── - - private void applyAssignedTaskPenalty(CardLockEntity l, AssignedTaskEntity task) { - if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) { - LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes()); - // Bestehenden Freeze nur verlängern, nie verkürzen - if (l.getFrozenUntill() == null || until.isAfter(l.getFrozenUntill())) { - l.setFrozenUntill(until); - l.setNextCardIn(until); - } - } - if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) { - List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); - for (int i = 0; i < task.getPenaltyRedCards(); i++) { - cards.add(CardEnum.RED); - } - l.setAvailableCards(cards); - } - } - - private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) { - systemMessageService.send(senderId, receiverId, text, targetUrl, cause); - } - - @GetMapping("/cardlock/unlock-history") - public ResponseEntity>> getUnlockHistory(Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - var entries = unlockCodeHistoryRepository.findByUserIdOrderByReceivedAtDesc(myId, - org.springframework.data.domain.PageRequest.of(0, 10)); - List> result = new ArrayList<>(); - for (var e : entries) { - Map item = new HashMap<>(); - item.put("lockName", e.getLockName()); - item.put("unlockCode", e.getUnlockCode()); - item.put("source", e.getSource()); - item.put("receivedAt", e.getReceivedAt().toString()); - result.add(item); - } - return ResponseEntity.ok(result); - } - - // ── Keyholder: Aufgabe stellen ───────────────────────────────────────────── - - record AssignTaskRequest(int taskIndex, int acceptDeadlineMinutes, - Integer penaltyFreezeMinutes, Integer penaltyRedCards) {} - - @Transactional - @PostMapping("/as-keyholder/{lockId}/assign-task") - public ResponseEntity assignTask(@PathVariable UUID lockId, - @RequestBody AssignTaskRequest req, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!me.getUserId().equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - - var tasks = l.getTasks(); - if (tasks == null || tasks.isEmpty()) - return ResponseEntity.badRequest().body(Map.of("error", "Dieses Lock hat keine Aufgaben.")); - if (req.taskIndex() < 0 || req.taskIndex() >= tasks.size()) - return ResponseEntity.badRequest().body(Map.of("error", "Ungültiger Aufgaben-Index.")); - if (req.acceptDeadlineMinutes() < 1) - return ResponseEntity.badRequest().body(Map.of("error", "Die Annahme-Frist muss mindestens 1 Minute betragen.")); - - long pendingCount = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream() - .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())) - .count(); - if (pendingCount >= 5) - return ResponseEntity.badRequest().body(Map.of("error", "Es sind bereits 5 Aufgaben offen. Bitte warte, bis der Lockee eine davon annimmt oder ablehnt.")); - - Task task = tasks.get(req.taskIndex()); - AssignedTaskEntity assigned = new AssignedTaskEntity(); - assigned.setLockId(lockId); - assigned.setTaskTitle(task.resolveTitle()); - assigned.setTaskDescription(task.getDescription()); - assigned.setTaskText(task.resolveTitle()); // Compat - assigned.setTaskMinutes(task.getMinutes()); - assigned.setAssignedAt(LocalDateTime.now()); - assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes())); - assigned.setPenaltyFreezeMinutes(req.penaltyFreezeMinutes()); - assigned.setPenaltyRedCards(req.penaltyRedCards()); - assigned.setStatus("PENDING"); - assignedTaskRepository.save(assigned); - - sendMessage(me.getUserId(), l.getLockee(), - me.getName() + " hat dir eine Aufgabe gestellt. Du hast " + - req.acceptDeadlineMinutes() + " Minuten, um sie anzunehmen.", - "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - // ── Lockee: Aufgabe annehmen ─────────────────────────────────────────────── - - @Transactional - @PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/accept") - public ResponseEntity acceptAssignedTask(@PathVariable UUID lockId, - @PathVariable UUID taskId, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - var taskOpt = assignedTaskRepository.findById(taskId); - if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) - return ResponseEntity.notFound().build(); - var task = taskOpt.get(); - if (!"PENDING".equals(task.getStatus())) - return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend.")); - if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) { - // Bereits abgelaufen – Strafe anwenden - task.setStatus("EXPIRED"); - applyAssignedTaskPenalty(l, task); - assignedTaskRepository.save(task); - cardlockRepository.save(l); - return ResponseEntity.status(409).body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet.")); - } - boolean hasActiveTask = (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) - || (l.getTaskFrozenUntil() != null && l.getTaskFrozenUntil().isAfter(LocalDateTime.now())); - if (hasActiveTask) - return ResponseEntity.status(409).body(Map.of("error", "Du hast bereits eine laufende Aufgabe.")); - - // Aufgabe aktivieren – separater Task-Timer, kein Freeze - String title = task.getTaskTitle() != null ? task.getTaskTitle() : task.getTaskText(); - l.setCurrentTask(title); - l.setCurrentTaskDescription(task.getTaskDescription()); - if (task.getTaskMinutes() != null && task.getTaskMinutes() > 0) { - l.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getTaskMinutes())); - - // Fälligkeit aller anderen offenen Aufgaben um die Task-Dauer verschieben - final int extraMinutes = task.getTaskMinutes(); - assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream() - .filter(t -> !t.getTaskId().equals(taskId)) - .forEach(t -> { - t.setAcceptDeadline(t.getAcceptDeadline().plusMinutes(extraMinutes)); - assignedTaskRepository.save(t); - }); - } - task.setStatus("ACCEPTED"); - assignedTaskRepository.save(task); - cardlockRepository.save(l); - - sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - return ResponseEntity.noContent().build(); - } - - // ── Lockee: Aufgabe ablehnen ─────────────────────────────────────────────── - - @Transactional - @PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/decline") - public ResponseEntity declineAssignedTask(@PathVariable UUID lockId, - @PathVariable UUID taskId, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - - var taskOpt = assignedTaskRepository.findById(taskId); - if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) - return ResponseEntity.notFound().build(); - var task = taskOpt.get(); - if (!"PENDING".equals(task.getStatus())) - return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend.")); - - task.setStatus("DECLINED"); - applyAssignedTaskPenalty(l, task); - assignedTaskRepository.save(task); - cardlockRepository.save(l); - - sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - return ResponseEntity.noContent().build(); - } - - // ── Keyholder: Aufgabe zurückziehen ─────────────────────────────────────── - - @Transactional - @DeleteMapping("/as-keyholder/{lockId}/assigned-tasks/{taskId}") - public ResponseEntity cancelAssignedTask(@PathVariable UUID lockId, - @PathVariable UUID taskId, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - - var taskOpt = assignedTaskRepository.findById(taskId); - if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) - return ResponseEntity.notFound().build(); - var task = taskOpt.get(); - if (!"PENDING".equals(task.getStatus())) - return ResponseEntity.status(409).body(Map.of("error", "Aufgabe ist nicht mehr ausstehend.")); - - assignedTaskRepository.delete(task); - return ResponseEntity.noContent().build(); - } - - record FreezeRequest(String frozenUntil) {} - - @Transactional - @PostMapping("/as-keyholder/{lockId}/freeze") - public ResponseEntity freezeLock(@PathVariable UUID lockId, - @RequestBody FreezeRequest req, - Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) { - return ResponseEntity.badRequest().body(Map.of("error", "Das Lock ist gerade durch eine Aufgabe eingefroren.")); - } - - LocalDateTime until; - try { - until = LocalDateTime.parse(req.frozenUntil()); - } catch (Exception e) { - return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat.")); - } - if (!until.isAfter(LocalDateTime.now())) { - return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen.")); - } - - l.setFrozenUntill(until); - cardlockRepository.save(l); - - sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock bis " + - until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + - " " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) + - " Uhr eingefroren.", - "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - @Transactional - @DeleteMapping("/as-keyholder/{lockId}/freeze") - public ResponseEntity unfreezeLock(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) { - return ResponseEntity.badRequest().body(Map.of("error", "Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden.")); - } - - l.setFrozenUntill(null); - cardlockRepository.save(l); - - sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.", - "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - @Transactional - @PostMapping("/as-keyholder/{lockId}/request-unlock") - public ResponseEntity requestUnlock(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID myId = meOpt.get().getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); - - l.setKeyholderRequestedUnlock(true); - cardlockRepository.save(l); - - sendMessage(myId, l.getLockee(), - "Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.", - "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); - - return ResponseEntity.noContent().build(); - } - - @Transactional - @PostMapping("/cardlock/{lockId}/emergency-unlock") - public ResponseEntity requestEmergencyUnlock(@PathVariable UUID lockId, Principal principal) { - var meOpt = userRepository.findByEmail(principal.getName()); - if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var me = meOpt.get(); - UUID myId = me.getUserId(); - - var lockOpt = cardlockRepository.findById(lockId); - if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); - var l = lockOpt.get(); - if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - if (l.isTestLock()) return ResponseEntity.badRequest().build(); - if (l.getEmergencyUnlockRequestedAt() != null) return ResponseEntity.status(409).build(); - - l.setEmergencyUnlockRequestedAt(LocalDateTime.now()); - - if (l.getKeyholder() == null) { - // Self-Lock ohne Keyholderin → sofort öffnen - l.setEmergencyAutoUnlocked(true); - l.setKeyholderRequestedUnlock(true); - } else { - // Keyholderin benachrichtigen - sendMessage(myId, l.getKeyholder(), - "⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.", - "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY); - } - cardlockRepository.save(l); - return ResponseEntity.noContent().build(); - } - - private String cardLabel(CardEnum card) { - return switch (card) { - case RED -> "Rote Karte"; - case GREEN -> "Grüne Karte"; - case YELLOW -> "Gelbe Karte"; - case TASK -> "Aufgabe"; - case FREEZE -> "Freeze"; - case RESET -> "Reset"; - case DOUBLE_UP -> "Double Up"; - }; - } - - @GetMapping("/invitation/{token}") - public void confirmInvitation(@PathVariable String token, - jakarta.servlet.http.HttpServletResponse response) throws Exception { - var invOpt = invitationRepository.findByToken(token); - if (invOpt.isEmpty()) { - response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid"); - return; - } - var inv = invOpt.get(); - var lockOpt = cardlockRepository.findById(inv.getLockId()); - if (lockOpt.isEmpty()) { - response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid"); - return; - } - var lock = lockOpt.get(); - lock.setKeyholder(inv.getKeyholderUserId()); - cardlockRepository.save(lock); - invitationRepository.delete(inv); - response.sendRedirect("/keyholder.html"); - } + private final CardlockRepository cardlockRepository; + private final UserRepository userRepository; + private final KeyholderInvitationRepository invitationRepository; + private final VerificationRepository verificationRepository; + private final VerificationVoteRepository verificationVoteRepository; + private final HygieneViolationRepository hygieneViolationRepository; + private final LockeeInvitationRepository lockeeInvitationRepository; + private final AssignedTaskRepository assignedTaskRepository; + private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; + private final CommunityTaskVoteRepository communityTaskVoteRepository; + private final UnlockCodeHistoryRepository unlockCodeHistoryRepository; + private final UnlockCodeHistoryService unlockCodeHistoryService; + private final SystemMessageService systemMessageService; + private final CardLockServiceFactory cardLockServiceFactory; + + @Value("${app.base-url:http://localhost:8080}") + private String baseUrl; + + public CardLockController(CardlockRepository cardlockRepository, UserRepository userRepository, + KeyholderInvitationRepository invitationRepository, VerificationRepository verificationRepository, + VerificationVoteRepository verificationVoteRepository, + HygieneViolationRepository hygieneViolationRepository, + LockeeInvitationRepository lockeeInvitationRepository, AssignedTaskRepository assignedTaskRepository, + KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, + CommunityTaskVoteRepository communityTaskVoteRepository, + UnlockCodeHistoryRepository unlockCodeHistoryRepository, UnlockCodeHistoryService unlockCodeHistoryService, + + SystemMessageService systemMessageService, CardLockServiceFactory cardLockServiceFactory) { + this.cardlockRepository = cardlockRepository; + this.userRepository = userRepository; + this.invitationRepository = invitationRepository; + this.verificationRepository = verificationRepository; + this.verificationVoteRepository = verificationVoteRepository; + this.hygieneViolationRepository = hygieneViolationRepository; + this.lockeeInvitationRepository = lockeeInvitationRepository; + this.assignedTaskRepository = assignedTaskRepository; + this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; + this.communityTaskVoteRepository = communityTaskVoteRepository; + this.unlockCodeHistoryRepository = unlockCodeHistoryRepository; + this.unlockCodeHistoryService = unlockCodeHistoryService; + this.systemMessageService = systemMessageService; + this.cardLockServiceFactory = cardLockServiceFactory; + } + + record CreateCardLockRequest(String name, UUID keyholder, UUID lockeeUserId, boolean lockeeDetailsVisible, + List initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards, + LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites, + List tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines, + String taskCardMode) { + } + + private static final SecureRandom RNG = new SecureRandom(); + + private String generateUnlockCode(int lines) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lines; i++) { + sb.append(RNG.nextInt(10)); + } + return sb.toString(); + } + + @PostMapping("/cardlock") + public ResponseEntity> createCardLock(@RequestBody CreateCardLockRequest req, + Principal principal) { + + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + if (req.initialCards() == null || req.initialCards().isEmpty() || req.pickEveryMinute() == null + || req.pickEveryMinute() < 1) + return ResponseEntity.badRequest().build(); + + // Friend-lockee path: current user becomes keyholder, invite lockee + boolean friendLockee = req.lockeeUserId() != null && !req.lockeeUserId().equals(myId); + if (friendLockee) { + var lockeeOpt = userRepository.findById(req.lockeeUserId()); + if (lockeeOpt.isEmpty()) + return ResponseEntity.badRequest().build(); + var lockee = lockeeOpt.get(); + + LocalDateTime now = LocalDateTime.now(); + CardLockEntity lock = new CardLockEntity(); + lock.setName(req.name()); + lock.setLockee(lockee.getUserId()); + lock.setKeyholder(myId); + lock.setInitialCards(req.initialCards()); + lock.setPickEveryMinute(req.pickEveryMinute()); + lock.setAccumulatePicks(req.accumulatePicks()); + lock.setShowRemainingCards(req.showRemainingCards()); + lock.setLatestOpeningtime(req.latestOpeningtime()); + lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); + lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); + lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); + lock.setRequiresVerification(req.requiresVerification()); + lock.setTestLock(false); + lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM"); + // startTime, unlockCode, unlockCodeLines left null until lockee accepts + cardlockRepository.save(lock); + + String token = UUID.randomUUID().toString().replace("-", ""); + LockeeInvitationEntity inv = new LockeeInvitationEntity(); + inv.setLockId(lock.getLockId()); + inv.setLockeeUserId(lockee.getUserId()); + inv.setKeyholderUserId(myId); + inv.setToken(token); + inv.setCreatedAt(now); + inv.setDetailsVisible(req.lockeeDetailsVisible()); + lockeeInvitationRepository.save(inv); + + String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; + sendMessage(myId, lockee.getUserId(), + me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.", + "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION); + + return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "lockeeInvitationSent", true)); + } + + // Self-lockee path (existing behavior) + if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) + return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + + int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5; + String unlockCode = generateUnlockCode(codeLines); + + CardLockEntity lock = new CardLockEntity(); + lock.setName(req.name()); + lock.setLockee(myId); + lock.setKeyholder(null); // set only after invitation is confirmed + lock.setInitialCards(req.initialCards()); + lock.setPickEveryMinute(req.pickEveryMinute()); + lock.setAccumulatePicks(req.accumulatePicks()); + lock.setShowRemainingCards(req.showRemainingCards()); + lock.setLatestOpeningtime(req.latestOpeningtime()); + lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); + lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); + lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); + lock.setRequiresVerification(req.requiresVerification()); + lock.setTestLock(req.testLock()); + lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM"); + lock.setUnlockCodeLines(codeLines); + lock.setUnlockCode(unlockCode); + + LocalDateTime now = LocalDateTime.now(); + lock.setStartTime(now); + lock.setAvailableCards(new ArrayList<>(req.initialCards())); + lock.setOpenPicks(0); + lock.setNextCardIn(now.plusMinutes(req.pickEveryMinute())); + if (req.hygineOpeningEveryMinites() != null) { + lock.setLastHygineOpening(now); + } + + cardlockRepository.save(lock); + + boolean keyholderPending = false; + if (req.keyholder() != null) { + var khOpt = userRepository.findById(req.keyholder()); + if (khOpt.isPresent()) { + var kh = khOpt.get(); + String token = UUID.randomUUID().toString().replace("-", ""); + + KeyholderInvitationEntity inv = new KeyholderInvitationEntity(); + inv.setLockId(lock.getLockId()); + inv.setKeyholderUserId(kh.getUserId()); + inv.setLockeeUserId(myId); + inv.setToken(token); + inv.setCreatedAt(now); + invitationRepository.save(inv); + + String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; + sendMessage(me.getUserId(), kh.getUserId(), + me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.", + "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION); + + keyholderPending = true; + } + } + + return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "unlockCode", unlockCode, + "keyholderPending", keyholderPending)); + } + + @PostMapping("/cardlock/{lockId}/draw") + @Transactional + public ResponseEntity> drawCard(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + CardLockService service = cardLockServiceFactory.create(l); + CardDTO dto = service.getNextCard(); + if (dto == null) + return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar")); + + // Task-Karte in nicht-zufälligem Modus → Entscheidung delegieren + String taskPending = null; + if (dto.card() == CardEnum.TASK && !"RANDOM".equals(l.getTaskCardMode()) && l.getTasks() != null + && !l.getTasks().isEmpty()) { + + if ("KEYHOLDER".equals(l.getTaskCardMode()) && l.getKeyholder() != null) { + KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity(); + choice.setLockId(l.getLockId()); + choice.setCreatedAt(LocalDateTime.now()); + choice.setStatus("PENDING"); + keyholderTaskChoiceRepository.save(choice); + userRepository.findById(l.getKeyholder()) + .ifPresent(kh -> sendMessage(l.getLockee(), kh.getUserId(), + "Deine Lockee hat eine Aufgaben-Karte gezogen – wähle eine Aufgabe aus.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE)); + taskPending = "KEYHOLDER"; + + } else if ("COMMUNITY".equals(l.getTaskCardMode())) { + CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity(); + vote.setLockId(l.getLockId()); + vote.setCreatedAt(LocalDateTime.now()); + vote.setExpiresAt(LocalDateTime.now().plusHours(1)); + vote.setStatus("ACTIVE"); + vote.setTestLock(l.isTestLock()); + communityTaskVoteRepository.save(vote); + taskPending = l.isTestLock() ? "RANDOM" : "COMMUNITY"; + } + } + + Map result = new HashMap<>(); + result.put("card", dto.card().name()); + result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : ""); + if (taskPending != null) + result.put("taskPending", taskPending); + + // Grüne Karte → Entsperrcode-Historie speichern + Keyholder benachrichtigen + if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) { + unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD"); + if (l.getKeyholder() != null) { + sendMessage(myId, l.getKeyholder(), + meOpt.get().getName() + " hat die grüne Karte gezogen! Der Entsperrcode wurde angezeigt.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + } + } + + return ResponseEntity.ok(result); + } + + @PostMapping("/cardlock/{lockId}/hygiene/start") + @Transactional + public ResponseEntity> startHygieneOpening(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + l.setHygineOpeningtime(LocalDateTime.now()); + cardlockRepository.save(l); + + unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "HYGIENE_OPEN"); + + return ResponseEntity.ok(Map.of("unlockCode", l.getUnlockCode(), "durationMinutes", + l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 30)); + } + + @PostMapping("/cardlock/{lockId}/hygiene/end") + @Transactional + public ResponseEntity> endHygieneOpening(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + LocalDateTime now = LocalDateTime.now(); + + // Overtime berechnen + if (l.getHygineOpeningtime() != null && l.getHygineOpeningDurationMinutes() != null) { + LocalDateTime dueTime = l.getHygineOpeningtime().plusMinutes(l.getHygineOpeningDurationMinutes()); + if (now.isAfter(dueTime)) { + long overtimeMinutes = ChronoUnit.MINUTES.between(dueTime, now); + if (l.getKeyholder() == null) { + // Self-Lock: 4-fache Überschreitungszeit einfrieren + if (l.getFrozenUntill() != null) { + l.setFrozenUntill(l.getFrozenUntill().plusMinutes(overtimeMinutes * 4)); + } else { + l.setFrozenUntill(now.plusMinutes(overtimeMinutes * 4)); + } + } else { + // Keyholder vorhanden: Verletzung protokollieren + HygieneViolationEntity violation = new HygieneViolationEntity(); + violation.setLockId(lockId); + violation.setLockeeId(myId); + violation.setKeyholderUserId(l.getKeyholder()); + violation.setViolationTime(now); + violation.setOvertimeMinutes(overtimeMinutes); + hygieneViolationRepository.save(violation); + } + } + } + + // Nächsten Öffnungszeitpunkt setzen + l.setLastHygineOpening(LocalDateTime.now()); + l.setHygineOpeningtime(null); + + // Neuen Entsperrcode generieren + int codeLines = l.getUnlockCodeLines() != null ? l.getUnlockCodeLines() : 5; + String newCode = generateUnlockCode(codeLines); + l.setUnlockCode(newCode); + cardlockRepository.save(l); + + unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), newCode, "HYGIENE_CLOSE"); + + return ResponseEntity.ok(Map.of("newUnlockCode", newCode)); + } + + @PostMapping("/cardlock/{lockId}/task/complete") + @Transactional + public ResponseEntity completeTask(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + CardLockService service = cardLockServiceFactory.create(l); + service.clearTask(); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/cardlock/{lockId}/green/keep") + @Transactional + public ResponseEntity greenKeep(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + CardLockService service = cardLockServiceFactory.create(l); + service.putBackGreen(); + + // Grüne Karte zurückgelegt → Keyholder benachrichtigen + if (l.getKeyholder() != null) { + sendMessage(myId, l.getKeyholder(), + meOpt.get().getName() + " hat die grüne Karte zurückgelegt und bleibt im Lock.", "/keyholder.html", + de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + } + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/mylock") + public ResponseEntity> getMyActiveLock(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var locks = cardlockRepository.findByLockee(meOpt.get().getUserId()); + var active = locks.stream().filter(l -> l.getUnlockTime() == null).findFirst(); + if (active.isEmpty()) + return ResponseEntity.noContent().build(); + return ResponseEntity.ok(Map.of("lockId", active.get().getLockId().toString())); + } + + @GetMapping("/cardlock/{lockId}") + public ResponseEntity> getLock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + Map cardCounts = new LinkedHashMap<>(); + if (l.getAvailableCards() != null) { + l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); + } + long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0; + + // Hygiene-Berechnung + boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; + boolean hygieneOpeningDue = false; + long hygieneSecondsRemaining = 0; + if (hygieneEnabled) { + LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); + if (base != null) { + LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); + hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); + hygieneOpeningDue = hygieneSecondsRemaining <= 0; + } + } + + Map result = new HashMap<>(); + result.put("lockId", l.getLockId().toString()); + result.put("name", l.getName() != null ? l.getName() : ""); + result.put("showRemainingCards", l.isShowRemainingCards()); + result.put("availableCardCounts", cardCounts); + result.put("totalCards", totalCards); + result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); + result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : ""); + result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); + result.put("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null); + result.put("currentTaskDescription", l.getCurrentTaskDescription()); + result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null); + result.put("hygieneEnabled", hygieneEnabled); + result.put("hygieneOpeningDue", hygieneOpeningDue); + result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); + result.put("hygieneOpeningActive", l.getHygineOpeningtime() != null); + result.put("hygieneOpeningStarted", + l.getHygineOpeningtime() != null ? l.getHygineOpeningtime().toString() : null); + result.put("hygieneDurationMinutes", + l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0); + result.put("hasKeyholder", l.getKeyholder() != null); + result.put("keyholderInvitationPending", + l.getKeyholder() == null && !invitationRepository.findByLockId(l.getLockId()).isEmpty()); + if (l.getKeyholder() != null) { + userRepository.findById(l.getKeyholder()).ifPresent(kh -> { + result.put("keyholderName", kh.getName()); + result.put("keyholderUserId", kh.getUserId().toString()); + result.put("keyholderProfilePic", kh.getProfilePicture()); + }); + } + + // Verifikation + boolean verificationDue = false; + String verificationTodayId = null; + String verificationPendingId = null; + String verificationPendingCode = null; + long verificationUpvotes = 0; + long verificationDownvotes = 0; + if (l.isRequiresVerification()) { + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + LocalDateTime todayEnd = todayStart.plusDays(1); + var completed = verificationRepository + .findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd); + if (!completed.isEmpty()) { + var todayV = completed.get(0); + verificationTodayId = todayV.getVerficationId().toString(); + var votes = verificationVoteRepository.findAllByVerificationId(todayV.getVerficationId()); + verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); + verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); + } else { + verificationDue = true; + var pending = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(l.getLockId(), + todayStart, todayEnd); + if (!pending.isEmpty()) { + verificationPendingId = pending.get(0).getVerficationId().toString(); + verificationPendingCode = pending.get(0).getCode(); + } + } + } + result.put("verificationRequired", l.isRequiresVerification()); + result.put("verificationDue", verificationDue); + result.put("verificationTodayId", verificationTodayId); + result.put("verificationUpvotes", verificationUpvotes); + result.put("verificationDownvotes", verificationDownvotes); + result.put("verificationPendingId", verificationPendingId); + result.put("verificationPendingCode", verificationPendingCode); + + // Abgelaufene Aufgaben prüfen und Strafe anwenden + boolean lockDirty = false; + var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream() + .filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList(); + for (var t : expiredTasks) { + t.setStatus("EXPIRED"); + applyAssignedTaskPenalty(l, t); + assignedTaskRepository.save(t); + lockDirty = true; + sendMessage(l.getKeyholder(), l.getLockee(), + "Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.", + "/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + } + if (lockDirty) + cardlockRepository.save(l); + + // Ausstehende Keyholder-Aufgaben (ohne Aufgabentext) + var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream() + .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())).map(t -> { + Map m = new LinkedHashMap<>(); + m.put("taskId", t.getTaskId().toString()); + m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText()); + m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : ""); + m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0); + m.put("assignedAt", t.getAssignedAt().toString()); + m.put("acceptDeadline", t.getAcceptDeadline().toString()); + m.put("penaltyFreezeMinutes", + t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0); + m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0); + return m; + }).toList(); + result.put("assignedTasks", pendingAssigned); + result.put("taskCardMode", l.getTaskCardMode()); + + // Ausstehende Keyholder-Choices + boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository.findByLockIdAndStatus(l.getLockId(), "PENDING") + .isEmpty(); + result.put("pendingKeyholderChoice", pendingKeyholderChoice); + + // Aktive Community-Vote + var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE").stream() + .filter(v -> v.getLockId().equals(l.getLockId())).findFirst(); + if (activeVotes.isPresent()) { + var v = activeVotes.get(); + result.put("activeCommunityVote", + Map.of("voteSessionId", v.getVoteSessionId().toString(), "expiresAt", v.getExpiresAt().toString())); + } + + // Notfall-Entsperrung: nach 1 Stunde automatisch öffnen + if (l.getEmergencyUnlockRequestedAt() != null && !l.isKeyholderRequestedUnlock() + && l.getEmergencyUnlockRequestedAt().isBefore(LocalDateTime.now().minusHours(1))) { + l.setEmergencyAutoUnlocked(true); + l.setKeyholderRequestedUnlock(true); + cardlockRepository.save(l); + } + + // Keyholder hat Unlock angefordert → Unlock-Code mitliefern + result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock()); + if (l.isKeyholderRequestedUnlock()) { + result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); + // Notfall-Freigaben werden nicht in der Historie gespeichert + if (l.getEmergencyUnlockRequestedAt() == null) { + unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "KEYHOLDER_UNLOCK"); + } + } + + result.put("testLock", l.isTestLock()); + result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); + if (l.isTestLock()) { + result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); + } + + return ResponseEntity.ok(result); + } + + @PostMapping("/cardlock/{lockId}/verification/start") + @Transactional + public ResponseEntity> startVerification(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + LocalDateTime todayEnd = todayStart.plusDays(1); + + // Existierende Verifikation für heute zurückgeben statt neue anlegen + var existing = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(lockId, todayStart, + todayEnd); + if (!existing.isEmpty()) { + var ev = existing.get(0); + return ResponseEntity.ok(Map.of("verificationId", ev.getVerficationId().toString(), "code", ev.getCode())); + } + var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, + todayStart, todayEnd); + if (!completed.isEmpty()) { + var cv = completed.get(0); + return ResponseEntity.ok(Map.of("verificationId", cv.getVerficationId().toString(), "code", cv.getCode())); + } + + VerificationEntity v = new VerificationEntity(); + v.setVerficationId(UUID.randomUUID()); + v.setLockId(lockId); + v.setLockeeId(myId); + v.setCode(CodeCreator.createAlphanumericCode(6)); + v.setVerificationTime(LocalDateTime.now()); + if (l.getKeyholder() != null) + v.setKeyholderId(l.getKeyholder()); + verificationRepository.save(v); + + return ResponseEntity.ok(Map.of("verificationId", v.getVerficationId().toString(), "code", v.getCode())); + } + + @PostMapping(value = "/cardlock/{lockId}/verification/{verificationId}/complete", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Transactional + public ResponseEntity completeVerification(@PathVariable UUID lockId, @PathVariable UUID verificationId, + @RequestParam MultipartFile image, Principal principal) throws IOException { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + if (!lockOpt.get().getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + var vOpt = verificationRepository.findById(verificationId); + if (vOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var v = vOpt.get(); + if (!v.getLockId().equals(lockId)) + return ResponseEntity.status(403).build(); + + v.setImage(scaleImage(image.getBytes(), 1024)); + verificationRepository.save(v); + + var lock = lockOpt.get(); + if (lock.getKeyholder() != null) { + var lockee = meOpt.get(); + sendMessage(myId, lock.getKeyholder(), "📸 " + lockee.getName() + " hat eine Verifikation eingereicht.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + } + + return ResponseEntity.noContent().build(); + } + + private byte[] scaleImage(byte[] input, int maxSize) throws IOException { + BufferedImage original = ImageIO.read(new ByteArrayInputStream(input)); + if (original == null) + return input; + int w = original.getWidth(); + int h = original.getHeight(); + if (w <= maxSize && h <= maxSize) + return input; + double scale = (double) maxSize / Math.max(w, h); + int newW = (int) (w * scale); + int newH = (int) (h * scale); + BufferedImage scaled = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB); + Graphics2D g = scaled.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.drawImage(original, 0, 0, newW, newH, null); + g.dispose(); + String format = "jpeg"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(scaled, format, out); + return out.toByteArray(); + } + + @DeleteMapping("/cardlock/{lockId}/verification/today") + @Transactional + public ResponseEntity renewVerification(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + if (!lockOpt.get().getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + LocalDateTime todayEnd = todayStart.plusDays(1); + var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, + todayStart, todayEnd); + for (var v : completed) { + verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId()); + verificationRepository.delete(v); + } + return ResponseEntity.noContent().build(); + } + + // ── Keyholder-Dashboard Endpunkte ── + + @GetMapping("/invitations/mine") + public ResponseEntity>> getMyInvitations(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var invitations = invitationRepository.findByKeyholderUserId(myId); + List> result = new ArrayList<>(); + for (var inv : invitations) { + var lockOpt = cardlockRepository.findById(inv.getLockId()); + if (lockOpt.isEmpty()) + continue; + var lock = lockOpt.get(); + if (lock.getKeyholder() != null) + continue; // bereits akzeptiert + var lockeeOpt = userRepository.findById(lock.getLockee()); + if (lockeeOpt.isEmpty()) + continue; + var lockee = lockeeOpt.get(); + Map item = new HashMap<>(); + item.put("lockId", inv.getLockId().toString()); + item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); + item.put("lockeeName", lockee.getName()); + item.put("lockeeId", lockee.getUserId().toString()); + item.put("lockeeProfilePic", lockee.getProfilePicture()); + item.put("token", inv.getToken()); + item.put("createdAt", inv.getCreatedAt().toString()); + result.add(item); + } + return ResponseEntity.ok(result); + } + + @Transactional + @DeleteMapping("/invitations/mine/{token}") + public ResponseEntity declineInvitation(@PathVariable String token, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var invOpt = invitationRepository.findByToken(token); + if (invOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var inv = invOpt.get(); + if (!inv.getKeyholderUserId().equals(myId)) + return ResponseEntity.status(403).build(); + + var lockOpt = cardlockRepository.findById(inv.getLockId()); + invitationRepository.delete(inv); + + if (lockOpt.isPresent()) { + var lock = lockOpt.get(); + String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; + sendMessage(myId, lock.getLockee(), + me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.", + null, de.oaa.xxx.social.entity.MessageCause.INVITATION); + } + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/invitations/sent") + public ResponseEntity>> getSentKeyholderInvitations(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var invitations = invitationRepository.findByLockeeUserId(myId); + List> result = new ArrayList<>(); + for (var inv : invitations) { + var lockOpt = cardlockRepository.findById(inv.getLockId()); + if (lockOpt.isEmpty()) + continue; + var lock = lockOpt.get(); + if (lock.getKeyholder() != null) + continue; // already accepted + var khOpt = userRepository.findById(inv.getKeyholderUserId()); + if (khOpt.isEmpty()) + continue; + var kh = khOpt.get(); + Map item = new HashMap<>(); + item.put("lockId", lock.getLockId().toString()); + item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); + item.put("keyholderName", kh.getName()); + item.put("keyholderProfilePic", kh.getProfilePicture()); + item.put("token", inv.getToken()); + item.put("createdAt", inv.getCreatedAt().toString()); + result.add(item); + } + return ResponseEntity.ok(result); + } + + @Transactional + @DeleteMapping("/invitations/sent/{token}") + public ResponseEntity cancelSentKeyholderInvitation(@PathVariable String token, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var invOpt = invitationRepository.findByToken(token); + if (invOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var inv = invOpt.get(); + + // Verify the lock belongs to the current user as lockee + var lockOpt = cardlockRepository.findById(inv.getLockId()); + if (lockOpt.isEmpty() || !lockOpt.get().getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + invitationRepository.delete(inv); + + String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank() + ? lockOpt.get().getName() + : "Unbenanntes Lock"; + sendMessage(myId, inv.getKeyholderUserId(), + me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.", null, + de.oaa.xxx.social.entity.MessageCause.INVITATION); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/as-keyholder") + public ResponseEntity>> getLocksAsKeyholder(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var locks = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(myId); + List> result = new ArrayList<>(); + for (var lock : locks) { + var lockeeOpt = userRepository.findById(lock.getLockee()); + if (lockeeOpt.isEmpty()) + continue; + var lockee = lockeeOpt.get(); + Map item = new HashMap<>(); + item.put("lockId", lock.getLockId().toString()); + item.put("lockName", lock.getName() != null ? lock.getName() : "Unbenanntes Lock"); + item.put("lockeeName", lockee.getName()); + item.put("lockeeId", lockee.getUserId().toString()); + item.put("lockeeProfilePic", lockee.getProfilePicture()); + item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0); + item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null); + boolean frozenByKh = lock.getFrozenUntill() != null && lock.getFrozenUntill().isAfter(LocalDateTime.now()) + && (lock.getCurrentTask() == null || lock.getCurrentTask().isBlank()); + item.put("isFrozenByKeyholder", frozenByKh); + result.add(item); + } + return ResponseEntity.ok(result); + } + + @GetMapping("/as-keyholder/{lockId}") + public ResponseEntity> getLockAsKeyholder(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + + var lockeeOpt = userRepository.findById(l.getLockee()); + if (lockeeOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var lockee = lockeeOpt.get(); + + Map cardCounts = new LinkedHashMap<>(); + if (l.getAvailableCards() != null) { + l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); + } + + boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; + boolean hygieneOpeningDue = false; + long hygieneSecondsRemaining = 0; + if (hygieneEnabled) { + LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); + if (base != null) { + LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); + hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); + hygieneOpeningDue = hygieneSecondsRemaining <= 0; + } + } + + boolean verificationDue = false; + boolean verificationDoneToday = false; + String verificationMyVote = null; // null = not voted, "upvote", "downvote" + String verificationTodayId = null; + String verificationImage = null; + long verificationUpvotes = 0, verificationDownvotes = 0; + if (l.isRequiresVerification()) { + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + LocalDateTime todayEnd = todayStart.plusDays(1); + var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, + todayStart, todayEnd); + if (!completed.isEmpty()) { + verificationDoneToday = true; + var v = completed.get(0); + var votes = verificationVoteRepository.findAllByVerificationId(v.getVerficationId()); + verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); + verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); + verificationTodayId = v.getVerficationId().toString(); + var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getVerficationId(), myId); + if (myVoteOpt.isPresent()) { + verificationMyVote = myVoteOpt.get().isUpvote() ? "upvote" : "downvote"; + } else if (v.getImage() != null) { + verificationImage = java.util.Base64.getEncoder().encodeToString(v.getImage()); + } + } else { + verificationDue = true; + } + } + + var recentViolations = hygieneViolationRepository.findByLockId(lockId).stream() + .sorted((a, b) -> b.getViolationTime().compareTo(a.getViolationTime())).limit(5) + .map(v -> Map.of("time", v.getViolationTime().toString(), "overtimeMinutes", v.getOvertimeMinutes())) + .toList(); + + Map result = new HashMap<>(); + result.put("lockId", l.getLockId().toString()); + result.put("lockName", l.getName() != null ? l.getName() : "Unbenanntes Lock"); + result.put("lockeeName", lockee.getName()); + result.put("lockeeId", lockee.getUserId().toString()); + result.put("lockeeProfilePic", lockee.getProfilePicture()); + result.put("totalCards", l.getAvailableCards() != null ? l.getAvailableCards().size() : 0); + result.put("cardCounts", cardCounts); + result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); + result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null); + result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); + result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null); + boolean isFrozenByKeyholder = l.getFrozenUntill() != null && l.getFrozenUntill().isAfter(LocalDateTime.now()); + result.put("isFrozenByKeyholder", isFrozenByKeyholder); + result.put("currentTask", l.getCurrentTask()); + result.put("currentTaskDescription", l.getCurrentTaskDescription()); + result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null); + result.put("hygieneEnabled", hygieneEnabled); + result.put("hygieneOpeningDue", hygieneOpeningDue); + result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); + result.put("hygieneOpeningActive", l.getHygineOpeningtime() != null); + result.put("requiresVerification", l.isRequiresVerification()); + result.put("verificationDue", verificationDue); + result.put("verificationDoneToday", verificationDoneToday); + result.put("verificationTodayId", verificationTodayId); + result.put("verificationMyVote", verificationMyVote); + result.put("verificationImage", verificationImage); + result.put("verificationUpvotes", verificationUpvotes); + result.put("verificationDownvotes", verificationDownvotes); + result.put("hygieneViolations", recentViolations); + result.put("hasTasks", l.getTasks() != null && !l.getTasks().isEmpty()); + if (l.getTasks() != null) { + var taskList = l.getTasks().stream().map(t -> { + Map m = new LinkedHashMap<>(); + m.put("title", t.resolveTitle()); + m.put("description", t.getDescription() != null ? t.getDescription() : ""); + m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); + return m; + }).toList(); + result.put("taskList", taskList); + } else { + result.put("taskList", List.of()); + } + var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream() + .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())).map(t -> { + Map m = new LinkedHashMap<>(); + m.put("taskId", t.getTaskId().toString()); + m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText()); + m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : ""); + m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0); + m.put("assignedAt", t.getAssignedAt().toString()); + m.put("acceptDeadline", t.getAcceptDeadline().toString()); + m.put("penaltyFreezeMinutes", + t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0); + m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0); + return m; + }).toList(); + result.put("pendingAssignedTasks", pendingAssigned); + result.put("taskCardMode", l.getTaskCardMode()); + + // Ausstehende Task-Karten-Choices (KEYHOLDER-Modus) + List lockTasks = l.getTasks() != null ? l.getTasks() : List.of(); + List> taskListForChoice = new ArrayList<>(); + for (int i = 0; i < lockTasks.size(); i++) { + Task t = lockTasks.get(i); + Map tm = new LinkedHashMap<>(); + tm.put("index", i); + tm.put("title", t.resolveTitle()); + tm.put("description", t.getDescription() != null ? t.getDescription() : ""); + tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); + taskListForChoice.add(tm); + } + var pendingChoices = keyholderTaskChoiceRepository.findByLockIdAndStatus(lockId, "PENDING").stream().map(c -> { + Map cm = new LinkedHashMap<>(); + cm.put("choiceId", c.getChoiceId().toString()); + cm.put("createdAt", c.getCreatedAt().toString()); + cm.put("tasks", taskListForChoice); + return cm; + }).toList(); + result.put("pendingTaskChoices", pendingChoices); + result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock()); + result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); + result.put("emergencyUnlockRequestedAt", + l.getEmergencyUnlockRequestedAt() != null ? l.getEmergencyUnlockRequestedAt().toString() : null); + + return ResponseEntity.ok(result); + } + + @Transactional + @DeleteMapping("/cardlock/{lockId}") + public ResponseEntity deleteLock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + // Entsperrung protokollieren (History + XP) – gültig nur wenn Keyholder + // vorhanden und kein Auto-Notfall + CardLockService service = cardLockServiceFactory.create(l); + service.unlock(l.getUnlockCode()); + + var verifications = verificationRepository.findByLockId(lockId); + verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId())); + verificationRepository.deleteAll(verifications); + invitationRepository.deleteByLockId(lockId); + cardlockRepository.deleteById(lockId); + return ResponseEntity.noContent().build(); + } + + record ModifyCardsRequest(Map cards, boolean notifyDetailed) { + } + + @PostMapping("/as-keyholder/{lockId}/cards/add") + @Transactional + public ResponseEntity addCards(@PathVariable UUID lockId, @RequestBody ModifyCardsRequest req, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + if (req.cards() == null || req.cards().isEmpty()) + return ResponseEntity.badRequest().build(); + + List toAdd = new ArrayList<>(); + for (var entry : req.cards().entrySet()) { + try { + CardEnum type = CardEnum.valueOf(entry.getKey()); + int count = entry.getValue() != null ? Math.max(0, entry.getValue()) : 0; + for (int i = 0; i < count; i++) + toAdd.add(type); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } + if (toAdd.isEmpty()) + return ResponseEntity.badRequest().build(); + + List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); + cards.addAll(toAdd); + l.setAvailableCards(cards); + cardlockRepository.save(l); + + String detail = toAdd.stream().collect(Collectors.groupingBy(c -> c, Collectors.counting())).entrySet().stream() + .map(e -> e.getValue() + "x " + cardLabel(e.getKey())).collect(Collectors.joining(", ")); + String msgText = req.notifyDetailed() + ? me.getName() + " hat " + toAdd.size() + " Karte(n) zu deinem Lock hinzugefügt: " + detail + "." + : me.getName() + " hat Karten zu deinem Lock hinzugefügt."; + + sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId, + de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + @PostMapping("/as-keyholder/{lockId}/cards/remove") + @Transactional + public ResponseEntity> removeCards(@PathVariable UUID lockId, + @RequestBody ModifyCardsRequest req, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + if (req.cards() == null || req.cards().isEmpty()) + return ResponseEntity.badRequest().build(); + + List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); + + // Plausi: letzte grüne Karte darf nicht entfernt werden + long greenInDeck = cards.stream().filter(c -> c == CardEnum.GREEN).count(); + int greenToRemove = req.cards().getOrDefault("GREEN", 0); + if (greenInDeck > 0 && greenToRemove >= greenInDeck) { + return ResponseEntity.badRequest() + .body(Map.of("error", "Die letzte grüne Karte darf nicht entfernt werden.")); + } + + List removed = new ArrayList<>(); + for (var entry : req.cards().entrySet()) { + try { + CardEnum type = CardEnum.valueOf(entry.getKey()); + int count = entry.getValue() != null ? Math.max(0, entry.getValue()) : 0; + Iterator it = cards.iterator(); + int done = 0; + while (it.hasNext() && done < count) { + if (it.next() == type) { + it.remove(); + removed.add(type); + done++; + } + } + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } + if (removed.isEmpty()) + return ResponseEntity.badRequest().build(); + + l.setAvailableCards(cards); + cardlockRepository.save(l); + + String detail = removed.stream().collect(Collectors.groupingBy(c -> c, Collectors.counting())).entrySet() + .stream().map(e -> e.getValue() + "x " + cardLabel(e.getKey())).collect(Collectors.joining(", ")); + String msgText = req.notifyDetailed() + ? me.getName() + " hat " + removed.size() + " Karte(n) aus deinem Lock entfernt: " + detail + "." + : me.getName() + " hat Karten aus deinem Lock entfernt."; + + sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId, + de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + // ── Hilfsmethoden ────────────────────────────────────────────────────────── + + private void applyAssignedTaskPenalty(CardLockEntity l, AssignedTaskEntity task) { + if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) { + LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes()); + // Bestehenden Freeze nur verlängern, nie verkürzen + if (l.getFrozenUntill() == null || until.isAfter(l.getFrozenUntill())) { + l.setFrozenUntill(until); + l.setNextCardIn(until); + } + } + if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) { + List cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of()); + for (int i = 0; i < task.getPenaltyRedCards(); i++) { + cards.add(CardEnum.RED); + } + l.setAvailableCards(cards); + } + } + + private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, + de.oaa.xxx.social.entity.MessageCause cause) { + systemMessageService.send(senderId, receiverId, text, targetUrl, cause); + } + + @GetMapping("/cardlock/unlock-history") + public ResponseEntity>> getUnlockHistory(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + var entries = unlockCodeHistoryRepository.findByUserIdOrderByReceivedAtDesc(myId, + org.springframework.data.domain.PageRequest.of(0, 10)); + List> result = new ArrayList<>(); + for (var e : entries) { + Map item = new HashMap<>(); + item.put("lockName", e.getLockName()); + item.put("unlockCode", e.getUnlockCode()); + item.put("source", e.getSource()); + item.put("receivedAt", e.getReceivedAt().toString()); + result.add(item); + } + return ResponseEntity.ok(result); + } + + // ── Keyholder: Aufgabe stellen ───────────────────────────────────────────── + + record AssignTaskRequest(int taskIndex, int acceptDeadlineMinutes, Integer penaltyFreezeMinutes, + Integer penaltyRedCards) { + } + + @Transactional + @PostMapping("/as-keyholder/{lockId}/assign-task") + public ResponseEntity assignTask(@PathVariable UUID lockId, @RequestBody AssignTaskRequest req, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!me.getUserId().equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + + var tasks = l.getTasks(); + if (tasks == null || tasks.isEmpty()) + return ResponseEntity.badRequest().body(Map.of("error", "Dieses Lock hat keine Aufgaben.")); + if (req.taskIndex() < 0 || req.taskIndex() >= tasks.size()) + return ResponseEntity.badRequest().body(Map.of("error", "Ungültiger Aufgaben-Index.")); + if (req.acceptDeadlineMinutes() < 1) + return ResponseEntity.badRequest() + .body(Map.of("error", "Die Annahme-Frist muss mindestens 1 Minute betragen.")); + + long pendingCount = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream() + .filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now())).count(); + if (pendingCount >= 5) + return ResponseEntity.badRequest().body(Map.of("error", + "Es sind bereits 5 Aufgaben offen. Bitte warte, bis der Lockee eine davon annimmt oder ablehnt.")); + + Task task = tasks.get(req.taskIndex()); + AssignedTaskEntity assigned = new AssignedTaskEntity(); + assigned.setLockId(lockId); + assigned.setTaskTitle(task.resolveTitle()); + assigned.setTaskDescription(task.getDescription()); + assigned.setTaskText(task.resolveTitle()); // Compat + assigned.setTaskMinutes(task.getMinutes()); + assigned.setAssignedAt(LocalDateTime.now()); + assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes())); + assigned.setPenaltyFreezeMinutes(req.penaltyFreezeMinutes()); + assigned.setPenaltyRedCards(req.penaltyRedCards()); + assigned.setStatus("PENDING"); + assignedTaskRepository.save(assigned); + + sendMessage(me.getUserId(), l.getLockee(), + me.getName() + " hat dir eine Aufgabe gestellt. Du hast " + req.acceptDeadlineMinutes() + + " Minuten, um sie anzunehmen.", + "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + // ── Lockee: Aufgabe annehmen ─────────────────────────────────────────────── + + @Transactional + @PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/accept") + public ResponseEntity acceptAssignedTask(@PathVariable UUID lockId, @PathVariable UUID taskId, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + var taskOpt = assignedTaskRepository.findById(taskId); + if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) + return ResponseEntity.notFound().build(); + var task = taskOpt.get(); + if (!"PENDING".equals(task.getStatus())) + return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend.")); + if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) { + // Bereits abgelaufen – Strafe anwenden + task.setStatus("EXPIRED"); + applyAssignedTaskPenalty(l, task); + assignedTaskRepository.save(task); + cardlockRepository.save(l); + return ResponseEntity.status(409) + .body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet.")); + } + boolean hasActiveTask = (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) + || (l.getTaskFrozenUntil() != null && l.getTaskFrozenUntil().isAfter(LocalDateTime.now())); + if (hasActiveTask) + return ResponseEntity.status(409).body(Map.of("error", "Du hast bereits eine laufende Aufgabe.")); + + // Aufgabe aktivieren – separater Task-Timer, kein Freeze + String title = task.getTaskTitle() != null ? task.getTaskTitle() : task.getTaskText(); + l.setCurrentTask(title); + l.setCurrentTaskDescription(task.getTaskDescription()); + if (task.getTaskMinutes() != null && task.getTaskMinutes() > 0) { + l.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getTaskMinutes())); + + // Fälligkeit aller anderen offenen Aufgaben um die Task-Dauer verschieben + final int extraMinutes = task.getTaskMinutes(); + assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream() + .filter(t -> !t.getTaskId().equals(taskId)).forEach(t -> { + t.setAcceptDeadline(t.getAcceptDeadline().plusMinutes(extraMinutes)); + assignedTaskRepository.save(t); + }); + } + task.setStatus("ACCEPTED"); + assignedTaskRepository.save(task); + cardlockRepository.save(l); + + sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + return ResponseEntity.noContent().build(); + } + + // ── Lockee: Aufgabe ablehnen ─────────────────────────────────────────────── + + @Transactional + @PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/decline") + public ResponseEntity declineAssignedTask(@PathVariable UUID lockId, @PathVariable UUID taskId, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + + var taskOpt = assignedTaskRepository.findById(taskId); + if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) + return ResponseEntity.notFound().build(); + var task = taskOpt.get(); + if (!"PENDING".equals(task.getStatus())) + return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend.")); + + task.setStatus("DECLINED"); + applyAssignedTaskPenalty(l, task); + assignedTaskRepository.save(task); + cardlockRepository.save(l); + + sendMessage(myId, l.getKeyholder(), + meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + return ResponseEntity.noContent().build(); + } + + // ── Keyholder: Aufgabe zurückziehen ─────────────────────────────────────── + + @Transactional + @DeleteMapping("/as-keyholder/{lockId}/assigned-tasks/{taskId}") + public ResponseEntity cancelAssignedTask(@PathVariable UUID lockId, @PathVariable UUID taskId, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + + var taskOpt = assignedTaskRepository.findById(taskId); + if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId)) + return ResponseEntity.notFound().build(); + var task = taskOpt.get(); + if (!"PENDING".equals(task.getStatus())) + return ResponseEntity.status(409).body(Map.of("error", "Aufgabe ist nicht mehr ausstehend.")); + + assignedTaskRepository.delete(task); + return ResponseEntity.noContent().build(); + } + + record FreezeRequest(String frozenUntil) { + } + + @Transactional + @PostMapping("/as-keyholder/{lockId}/freeze") + public ResponseEntity freezeLock(@PathVariable UUID lockId, @RequestBody FreezeRequest req, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) { + return ResponseEntity.badRequest() + .body(Map.of("error", "Das Lock ist gerade durch eine Aufgabe eingefroren.")); + } + + LocalDateTime until; + try { + until = LocalDateTime.parse(req.frozenUntil()); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat.")); + } + if (!until.isAfter(LocalDateTime.now())) { + return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen.")); + } + + l.setFrozenUntill(until); + cardlockRepository.save(l); + + sendMessage(myId, l.getLockee(), + me.getName() + " hat dein Lock bis " + + until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + " " + + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) + + " Uhr eingefroren.", + "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + @Transactional + @DeleteMapping("/as-keyholder/{lockId}/freeze") + public ResponseEntity unfreezeLock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", + "Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden.")); + } + + l.setFrozenUntill(null); + cardlockRepository.save(l); + + sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.", + "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + @Transactional + @PostMapping("/as-keyholder/{lockId}/request-unlock") + public ResponseEntity requestUnlock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) + return ResponseEntity.status(403).build(); + + l.setKeyholderRequestedUnlock(true); + cardlockRepository.save(l); + + sendMessage(myId, l.getLockee(), + "Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.", + "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + @Transactional + @PostMapping("/cardlock/{lockId}/emergency-unlock") + public ResponseEntity requestEmergencyUnlock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) + return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) + return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) + return ResponseEntity.status(403).build(); + if (l.isTestLock()) + return ResponseEntity.badRequest().build(); + if (l.getEmergencyUnlockRequestedAt() != null) + return ResponseEntity.status(409).build(); + + l.setEmergencyUnlockRequestedAt(LocalDateTime.now()); + + if (l.getKeyholder() == null) { + // Self-Lock ohne Keyholderin → sofort öffnen + l.setEmergencyAutoUnlocked(true); + l.setKeyholderRequestedUnlock(true); + } else { + // Keyholderin benachrichtigen + sendMessage(myId, l.getKeyholder(), "⚠️ NOTFALL: " + me.getName() + + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.", + "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY); + } + cardlockRepository.save(l); + return ResponseEntity.noContent().build(); + } + + private String cardLabel(CardEnum card) { + return switch (card) { + case RED -> "Rote Karte"; + case GREEN -> "Grüne Karte"; + case YELLOW -> "Gelbe Karte"; + case TASK -> "Aufgabe"; + case FREEZE -> "Freeze"; + case RESET -> "Reset"; + case DOUBLE_UP -> "Double Up"; + }; + } + + @GetMapping("/invitation/{token}") + public void confirmInvitation(@PathVariable String token, jakarta.servlet.http.HttpServletResponse response) + throws Exception { + var invOpt = invitationRepository.findByToken(token); + if (invOpt.isEmpty()) { + response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid"); + return; + } + var inv = invOpt.get(); + var lockOpt = cardlockRepository.findById(inv.getLockId()); + if (lockOpt.isEmpty()) { + response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid"); + return; + } + var lock = lockOpt.get(); + lock.setKeyholder(inv.getKeyholderUserId()); + cardlockRepository.save(lock); + invitationRepository.delete(inv); + response.sendRedirect("/keyholder.html"); + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java new file mode 100644 index 0000000..fe094f1 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java @@ -0,0 +1,50 @@ +package de.oaa.xxx.games.chastity.cardlock; + +import de.oaa.xxx.games.history.GameHistoryRepository; +import de.oaa.xxx.games.chastity.verification.VerificationRepository; +import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository; +import de.oaa.xxx.user.UserRepository; +import org.springframework.stereotype.Service; + +/** + * Factory für CardLockService-Instanzen. + * + * CardLockService hält pro Instanz den Zustand eines konkreten CardLockEntity + * und kann daher kein Singleton-Bean sein. Diese Factory zentralisiert die + * Erzeugung und verwaltet alle Abhängigkeiten als injizierte Singletons. + */ +@Service +public class CardLockServiceFactory { + + private final VerificationRepository verificationRepository; + private final VerificationVoteRepository verificationVoteRepository; + private final CardLockRepository cardLockRepository; + private final GameHistoryRepository gameHistoryRepository; + private final UserRepository userRepository; + + public CardLockServiceFactory(VerificationRepository verificationRepository, + VerificationVoteRepository verificationVoteRepository, + CardLockRepository cardLockRepository, + GameHistoryRepository gameHistoryRepository, + UserRepository userRepository) { + this.verificationRepository = verificationRepository; + this.verificationVoteRepository = verificationVoteRepository; + this.cardLockRepository = cardLockRepository; + this.gameHistoryRepository = gameHistoryRepository; + this.userRepository = userRepository; + } + + /** + * Erstellt eine neue CardLockService-Instanz für das gegebene Lock. + */ + public CardLockService create(CardLockEntity lock) { + return new CardLockService( + lock, + verificationRepository, + verificationVoteRepository, + cardLockRepository, + gameHistoryRepository, + userRepository + ); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java index 8462c02..7eae9bf 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetConfirm.java @@ -1,3 +1,3 @@ package de.oaa.xxx.passwordreset; -public record PasswordResetConfirm(String token, String passwordHash) {} +public record PasswordResetConfirm(String token, String password) {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java index 0707d39..43866d9 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/passwordreset/PasswordResetController.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import java.util.UUID; @@ -25,15 +26,18 @@ public class PasswordResetController { private final UserRepository userRepository; private final MailService mailService; private final MailTemplateService mailTemplateService; + private final PasswordEncoder passwordEncoder; public PasswordResetController(PasswordResetRepository passwordResetRepository, UserRepository userRepository, MailService mailService, - MailTemplateService mailTemplateService) { + MailTemplateService mailTemplateService, + PasswordEncoder passwordEncoder) { this.passwordResetRepository = passwordResetRepository; this.userRepository = userRepository; this.mailService = mailService; this.mailTemplateService = mailTemplateService; + this.passwordEncoder = passwordEncoder; } @PostMapping("/request") @@ -67,7 +71,7 @@ public class PasswordResetController { return ResponseEntity.badRequest().build(); } userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> { - user.setPassword(confirm.passwordHash()); + user.setPassword(passwordEncoder.encode(confirm.password())); userRepository.save(user); LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail()); }); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/ActivationController.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/ActivationController.java index fc3d067..6572e61 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/ActivationController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/ActivationController.java @@ -1,44 +1,36 @@ -package de.oaa.xxx.registration; - -import java.net.URI; -import java.util.UUID; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import de.oaa.xxx.user.UserController; - - -@RestController -@RequestMapping("/activation") -public class ActivationController { - - private final RegistrationRepository registrationRepository; - private final UserController userController; - - public ActivationController(RegistrationRepository registrationRepository, UserController userController) { - this.registrationRepository = registrationRepository; - this.userController = userController; - } - - @GetMapping("/{uuid}") - public ResponseEntity activate(@PathVariable String uuid) { - RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null); - if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) { - ResponseEntity response = userController.userAnlegen(registration.toRegistration()); - if (response.getStatusCode().is2xxSuccessful()) { - registration.setActivated(Boolean.TRUE); - registrationRepository.save(registration); - String redirect = "/login.html?email=" + java.net.URLEncoder.encode(registration.getEmail(), java.nio.charset.StandardCharsets.UTF_8); - return ResponseEntity.status(302).location(URI.create(redirect)).build(); - } else { - return ResponseEntity.internalServerError().build(); - } - } else { - return ResponseEntity.noContent().build(); - } - } -} +package de.oaa.xxx.registration; + +import java.net.URI; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/activation") +public class ActivationController { + + private final RegistrationService registrationService; + + public ActivationController(RegistrationService registrationService) { + this.registrationService = registrationService; + } + + @GetMapping("/{uuid}") + public ResponseEntity activate(@PathVariable String uuid) { + try { + String email = registrationService.activate(uuid); + String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8); + return ResponseEntity.status(302).location(URI.create(redirect)).build(); + } catch (IllegalStateException e) { + // Bereits aktiviert → trotzdem zum Login weiterleiten (idempotent) + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.noContent().build(); + } catch (Exception e) { + return ResponseEntity.internalServerError().build(); + } + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/Registration.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/Registration.java index 182b551..39d1301 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/Registration.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/Registration.java @@ -13,7 +13,7 @@ public class Registration { private UUID id; private String name; private String email; - private String passwordHash; + private String password; private LocalDate geburtsdatum; @Override 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 000efe3..4aea067 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationController.java @@ -13,6 +13,7 @@ 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.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDate; import java.time.Period; @@ -30,13 +31,16 @@ public class RegistrationController { private final UserRepository userRepository; private final MailService mailService; private final MailTemplateService mailTemplateService; + private final PasswordEncoder passwordEncoder; public RegistrationController(RegistrationRepository registrationRepository, UserRepository userRepository, - MailService mailService, MailTemplateService mailTemplateService) { + MailService mailService, MailTemplateService mailTemplateService, + PasswordEncoder passwordEncoder) { this.registrationRepository = registrationRepository; this.userRepository = userRepository; this.mailService = mailService; this.mailTemplateService = mailTemplateService; + this.passwordEncoder = passwordEncoder; } @PostMapping @@ -57,6 +61,8 @@ public class RegistrationController { LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName()); return ResponseEntity.status(409).build(); } + // Passwort serverseitig mit BCrypt hashen + registration.setPassword(passwordEncoder.encode(registration.getPassword())); RegistrationEntity entity = RegistrationEntity.create(registration); registrationRepository.save(entity); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationEntity.java index a943ba2..eee1a6f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationEntity.java @@ -40,7 +40,7 @@ public class RegistrationEntity { registration.setId(registrationId); registration.setEmail(email); registration.setName(name); - registration.setPasswordHash(password); + registration.setPassword(password); registration.setGeburtsdatum(geburtsdatum); return registration; } @@ -51,7 +51,7 @@ public class RegistrationEntity { entity.setEmail(registration.getEmail()); entity.setActivated(Boolean.FALSE); entity.setName(registration.getName()); - entity.setPassword(registration.getPasswordHash()); + entity.setPassword(registration.getPassword()); entity.setGeburtsdatum(registration.getGeburtsdatum()); return entity; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationService.java b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationService.java new file mode 100644 index 0000000..9fa20e5 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/registration/RegistrationService.java @@ -0,0 +1,58 @@ +package de.oaa.xxx.registration; + +import de.oaa.xxx.user.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** + * Koordiniert den Aktivierungsflow: liest die RegistrationEntity aus der DB + * und delegiert die User-Anlage an den UserService. + * Ersetzt den direkten Controller→Controller-Aufruf im ActivationController. + */ +@Service +public class RegistrationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationService.class); + + private final RegistrationRepository registrationRepository; + private final UserService userService; + + public RegistrationService(RegistrationRepository registrationRepository, UserService userService) { + this.registrationRepository = registrationRepository; + this.userService = userService; + } + + /** + * Aktiviert eine Registrierung: legt den User an und markiert die Registration als aktiviert. + * + * @return E-Mail des aktivierten Users (für Redirect im Controller) + * @throws IllegalArgumentException wenn UUID ungültig oder Registration nicht gefunden + * @throws IllegalStateException wenn Registration bereits aktiviert + */ + public String activate(String uuid) { + UUID registrationId; + try { + registrationId = UUID.fromString(uuid); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Ungültige UUID: " + uuid); + } + + RegistrationEntity registration = registrationRepository.findById(registrationId) + .orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + uuid)); + + if (Boolean.TRUE.equals(registration.getActivated())) { + throw new IllegalStateException("Registration bereits aktiviert"); + } + + userService.createUser(registration.toRegistration()); + + registration.setActivated(Boolean.TRUE); + registrationRepository.save(registration); + + LOGGER.info("Registration {} aktiviert, User {} angelegt", uuid, registration.getEmail()); + return registration.getEmail(); + } +} 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 9c7f4a3..ebd3bca 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/LoginController.java @@ -1,83 +1,88 @@ -package de.oaa.xxx.user; - -import de.oaa.xxx.config.JwtService; -import jakarta.servlet.http.HttpServletResponse; -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.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.security.Principal; -import java.time.Duration; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/login") -public class LoginController { - - private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class); - - private final UserRepository userRepository; - private final JwtService jwtService; - - public LoginController(UserRepository userRepository, JwtService jwtService) { - this.userRepository = userRepository; - this.jwtService = jwtService; - } - - @GetMapping - public ResponseEntity login(@RequestParam String email, @RequestParam String hash, - HttpServletResponse response) { - Optional user = userRepository.findByEmailAndPassword(email, hash); - if (user.isPresent()) { - LOGGER.info("User erfolgreich angemeldet: {}", email); - String token = jwtService.generateToken(user.get().getEmail(), user.get().getName()); - ResponseCookie cookie = ResponseCookie.from("jwt", token) - .httpOnly(true) - .sameSite("Strict") - .path("/") - .maxAge(Duration.ofHours(24)) - .build(); - response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(user.get().toUser()); - } else { - return ResponseEntity.noContent().build(); - } - } - - @GetMapping("/me") - public ResponseEntity me(Principal principal) { - if (principal == null) { - return ResponseEntity.status(401).build(); - } - return userRepository.findByEmail(principal.getName()) - .map(entity -> ResponseEntity.ok(entity.toUser())) - .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) - .map(entity -> ResponseEntity.ok(entity.toUser())) - .orElse(ResponseEntity.noContent().build()); - } -} +package de.oaa.xxx.user; + +import de.oaa.xxx.config.JwtService; +import jakarta.servlet.http.HttpServletResponse; +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.security.crypto.password.PasswordEncoder; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.time.Duration; +import java.util.UUID; + +@RestController +@RequestMapping("/login") +public class LoginController { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class); + + record LoginRequest(String email, String password) {} + + private final UserRepository userRepository; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + + public LoginController(UserRepository userRepository, JwtService jwtService, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.jwtService = jwtService; + this.passwordEncoder = passwordEncoder; + } + + @PostMapping + public ResponseEntity login(@RequestBody LoginRequest request, HttpServletResponse response) { + var userOpt = userRepository.findByEmail(request.email()); + if (userOpt.isPresent() && passwordEncoder.matches(request.password(), userOpt.get().getPassword())) { + UserEntity user = userOpt.get(); + LOGGER.info("User erfolgreich angemeldet: {}", request.email()); + String token = jwtService.generateToken(user.getEmail(), user.getName()); + ResponseCookie cookie = ResponseCookie.from("jwt", token) + .httpOnly(true) + .sameSite("Strict") + .path("/") + .maxAge(Duration.ofHours(24)) + .build(); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + return ResponseEntity.ok(user.toUser()); + } else { + return ResponseEntity.noContent().build(); + } + } + + @GetMapping("/me") + public ResponseEntity me(Principal principal) { + if (principal == null) { + return ResponseEntity.status(401).build(); + } + return userRepository.findByEmail(principal.getName()) + .map(entity -> ResponseEntity.ok(entity.toUser())) + .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) + .map(entity -> ResponseEntity.ok(entity.toUser())) + .orElse(ResponseEntity.noContent().build()); + } +} 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 c9f88d5..6cc9f8b 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java @@ -1,443 +1,287 @@ -package de.oaa.xxx.user; - -import java.security.Principal; -import java.time.LocalDate; -import java.time.Period; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; - -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.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 de.oaa.xxx.aufgaben.repository.AufgabeRepository; -import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity; -import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository; -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.games.bdsm.entity.AktiveSperreEntity; -import de.oaa.xxx.games.bdsm.entity.MitspielerEntity; -import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository; -import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository; -import de.oaa.xxx.games.bdsm.repository.MitspielerRepository; -import de.oaa.xxx.passwordreset.PasswordResetRepository; -import de.oaa.xxx.registration.Registration; -import de.oaa.xxx.registration.RegistrationRepository; -import de.oaa.xxx.social.entity.MessageCause; -import de.oaa.xxx.social.entity.NotificationPreferenceEntity; -import de.oaa.xxx.social.repository.KommentarLikeRepository; -import de.oaa.xxx.social.repository.KommentarRepository; -import de.oaa.xxx.social.repository.NotificationPreferenceRepository; -import de.oaa.xxx.social.repository.PinnwandEintragRepository; -import de.oaa.xxx.social.repository.PinnwandLikeRepository; -import de.oaa.xxx.social.repository.ProfileImageLikeRepository; -import de.oaa.xxx.social.repository.ProfileImageRepository; -import jakarta.transaction.Transactional; - -@RestController -@RequestMapping("/user") -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 BdsmGameRepository 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; - private final PinnwandEintragRepository pinnwandEintragRepository; - private final PinnwandLikeRepository pinnwandLikeRepository; - private final KommentarRepository kommentarRepository; - private final KommentarLikeRepository kommentarLikeRepository; - private final NotificationPreferenceRepository notificationPreferenceRepository; - private final BdsmDefaultsRepository bdsmDefaultsRepository; - - public UserController(UserRepository userRepository, - RegistrationRepository registrationRepository, - AufgabenGruppeRepository aufgabenGruppeRepository, - AufgabeRepository aufgabeRepository, - StrafeRepository strafeRepository, - SperreRepository sperreRepository, - ToyRepository toyRepository, - FavoritRepository favoritRepository, - GruppenAboRepository gruppenAboRepository, - BdsmGameRepository sessionRepository, - AktiveSperreRepository aktiveSperreRepository, - MitspielerRepository mitspielerRepository, - EmailChangeRepository emailChangeRepository, - PasswordResetRepository passwordResetRepository, - ProfileImageRepository profileImageRepository, - ProfileImageLikeRepository profileImageLikeRepository, - PinnwandEintragRepository pinnwandEintragRepository, - PinnwandLikeRepository pinnwandLikeRepository, - KommentarRepository kommentarRepository, - KommentarLikeRepository kommentarLikeRepository, - NotificationPreferenceRepository notificationPreferenceRepository, - BdsmDefaultsRepository bdsmDefaultsRepository) { - 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; - this.pinnwandEintragRepository = pinnwandEintragRepository; - this.pinnwandLikeRepository = pinnwandLikeRepository; - this.kommentarRepository = kommentarRepository; - this.kommentarLikeRepository = kommentarLikeRepository; - this.notificationPreferenceRepository = notificationPreferenceRepository; - this.bdsmDefaultsRepository = bdsmDefaultsRepository; - } - - record ProfilePictureRequest(String picture, String pictureHq) {} - record NameChangeRequest(String name) {} - record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {} - record ProfileRequest(Integer groesse, Integer gewicht, - Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} - record PrivacyRequest( - Sichtbarkeit sichtbarkeitGrunddaten, - Sichtbarkeit sichtbarkeitGalerie, - Sichtbarkeit sichtbarkeitFreunde, - Sichtbarkeit sichtbarkeitFeed, - Sichtbarkeit sichtbarkeitPinnwand, - Sichtbarkeit sichtbarkeitXp, - Sichtbarkeit sichtbarkeitLockhistorie) {} - - @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()); - LOGGER.debug("User {} hat Profilbild aktualisiert", user.get().getUserId()); - return ResponseEntity.ok().build(); - } - - @PutMapping("/me/profile") - public ResponseEntity updateProfile(@RequestBody ProfileRequest request, Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - var user = userOpt.get(); - if (request.beschreibung() != null && request.beschreibung().length() > 600) { - return ResponseEntity.badRequest().build(); - } - user.setGroesse(request.groesse()); - user.setGewicht(request.gewicht()); - user.setGeschlecht(request.geschlecht()); - user.setNeigung(request.neigung()); - user.setBeziehungsstatus(request.beziehungsstatus()); - user.setBeschreibung(request.beschreibung()); - userRepository.save(user); - LOGGER.info("User {} hat Profil aktualisiert", user.getUserId()); - return ResponseEntity.ok().build(); - } - - @PutMapping("/me/privacy") - public ResponseEntity updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - var user = userOpt.get(); - if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten()); - if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie()); - if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde()); - if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed()); - if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand()); - if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp()); - if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie()); - userRepository.save(user); - LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId()); - return ResponseEntity.ok().build(); - } - - record NotificationPreferenceRequest(boolean inApp, boolean email) {} - - @GetMapping("/me/notifications") - public ResponseEntity> getNotifications(Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID userId = userOpt.get().getUserId(); - - Map byKey = notificationPreferenceRepository.findByUserId(userId) - .stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p)); - - Map result = new LinkedHashMap<>(); - for (MessageCause cause : MessageCause.values()) { - NotificationPreferenceEntity pref = byKey.getOrDefault( - cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause)); - Map entry = new LinkedHashMap<>(); - entry.put("inApp", pref.isInApp()); - entry.put("email", pref.isEmail()); - result.put(cause.name(), entry); - } - return ResponseEntity.ok(result); - } - - @PutMapping("/me/notifications") - public ResponseEntity updateNotifications(@RequestBody Map request, Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID userId = userOpt.get().getUserId(); - - for (var entry : request.entrySet()) { - MessageCause cause; - try { - cause = MessageCause.valueOf(entry.getKey()); - } catch (IllegalArgumentException e) { - continue; - } - NotificationPreferenceEntity pref = notificationPreferenceRepository - .findByUserIdAndCause(userId, cause) - .orElseGet(() -> { - NotificationPreferenceEntity n = new NotificationPreferenceEntity(); - n.setUserId(userId); - n.setCause(cause); - return n; - }); - pref.setInApp(entry.getValue().inApp()); - pref.setEmail(entry.getValue().email()); - notificationPreferenceRepository.save(pref); - } - return ResponseEntity.ok().build(); - } - - record BdsmDefaultsRequest(List spieltMit, List rollen, List werkzeuge) {} - - @GetMapping("/me/bdsm-defaults") - public ResponseEntity> getBdsmDefaults(Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID userId = userOpt.get().getUserId(); - - BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) - .orElse(new BdsmDefaultsEntity()); - Map result = new java.util.LinkedHashMap<>(); - result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); - result.put("rollen", splitOrEmpty(d.getRollen())); - result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); - return ResponseEntity.ok(result); - } - - @GetMapping("/{userId}/bdsm-defaults") - public ResponseEntity> getBdsmDefaultsForUser(@PathVariable UUID userId) { - var userOpt = userRepository.findById(userId); - if (userOpt.isEmpty()) return ResponseEntity.notFound().build(); - UserEntity user = userOpt.get(); - BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) - .orElse(new BdsmDefaultsEntity()); - Map result = new java.util.LinkedHashMap<>(); - result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null); - result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); - result.put("rollen", splitOrEmpty(d.getRollen())); - result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); - return ResponseEntity.ok(result); - } - - @PutMapping("/me/bdsm-defaults") - public ResponseEntity updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) { - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - UUID userId = userOpt.get().getUserId(); - - BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) - .orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; }); - d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit())); - d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen())); - d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge())); - bdsmDefaultsRepository.save(d); - return ResponseEntity.ok().build(); - } - - private static List splitOrEmpty(String s) { - if (s == null || s.isBlank()) return List.of(); - return List.of(s.split(",")); - } - - @PutMapping("/me/geburtsdatum") - public ResponseEntity updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) { - if (request.geburtsdatum() == null - || Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) { - return ResponseEntity.status(422).build(); - } - var userOpt = userRepository.findByEmail(principal.getName()); - if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); - var user = userOpt.get(); - user.setGeburtsdatum(request.geburtsdatum()); - userRepository.save(user); - LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId()); - 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()); - LOGGER.info("User {} hat Namen zu '{}' geändert", user.get().getUserId(), newName); - 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()); - kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId()) - .forEach(k -> { - kommentarLikeRepository.deleteByKommentarId(k.getKommentarId()); - kommentarRepository.delete(k); - }); - } - profileImageRepository.deleteAll(profileImages); - profileImageLikeRepository.deleteByUserId(userId); - - // 5c. Delete pinnwand entries (authored by or on user's wall) + their likes/comments - var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId); - for (var e : ownWallEntries) { - pinnwandLikeRepository.deleteByEintragId(e.getEintragId()); - kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId()) - .forEach(k -> { - kommentarLikeRepository.deleteByKommentarId(k.getKommentarId()); - kommentarRepository.delete(k); - }); - } - pinnwandEintragRepository.deleteAll(ownWallEntries); - pinnwandEintragRepository.deleteByAuthorId(userId); - pinnwandLikeRepository.deleteByUserId(userId); - kommentarRepository.deleteByAuthorId(userId); - kommentarLikeRepository.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 - public ResponseEntity userAnlegen(@RequestBody Registration registration) { - if (registration.getEmail() == null || registration.getPasswordHash() == null || registration.getName() == null) { - return ResponseEntity.badRequest().build(); - } - if (userRepository.findByEmail(registration.getEmail()).isPresent()) { - LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail()); - return ResponseEntity.status(409).build(); - } - try { - UserEntity entity = new UserEntity(); - entity.setUserId(UUID.randomUUID()); - entity.setEmail(registration.getEmail()); - entity.setName(registration.getName()); - entity.setPassword(registration.getPasswordHash()); - entity.setGeburtsdatum(registration.getGeburtsdatum()); - userRepository.save(entity); - - for (MessageCause cause : MessageCause.values()) { - notificationPreferenceRepository.save( - NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause)); - } - - return ResponseEntity.status(201).build(); - } catch (Exception exception) { - LOGGER.error(exception.getMessage(), exception); - return ResponseEntity.internalServerError().build(); - } - } -} +package de.oaa.xxx.user; + +import java.security.Principal; +import java.time.LocalDate; +import java.time.Period; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +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.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 de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity; +import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository; +import de.oaa.xxx.registration.Registration; +import de.oaa.xxx.registration.RegistrationRepository; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.social.entity.NotificationPreferenceEntity; +import de.oaa.xxx.social.repository.NotificationPreferenceRepository; + +@RestController +@RequestMapping("/user") +public class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + private final UserRepository userRepository; + private final RegistrationRepository registrationRepository; + private final NotificationPreferenceRepository notificationPreferenceRepository; + private final BdsmDefaultsRepository bdsmDefaultsRepository; + private final UserService userService; + + public UserController(UserRepository userRepository, + RegistrationRepository registrationRepository, + NotificationPreferenceRepository notificationPreferenceRepository, + BdsmDefaultsRepository bdsmDefaultsRepository, + UserService userService) { + this.userRepository = userRepository; + this.registrationRepository = registrationRepository; + this.notificationPreferenceRepository = notificationPreferenceRepository; + this.bdsmDefaultsRepository = bdsmDefaultsRepository; + this.userService = userService; + } + + record ProfilePictureRequest(String picture, String pictureHq) {} + record NameChangeRequest(String name) {} + record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {} + record ProfileRequest(Integer groesse, Integer gewicht, + Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} + record PrivacyRequest( + Sichtbarkeit sichtbarkeitGrunddaten, + Sichtbarkeit sichtbarkeitGalerie, + Sichtbarkeit sichtbarkeitFreunde, + Sichtbarkeit sichtbarkeitFeed, + Sichtbarkeit sichtbarkeitPinnwand, + Sichtbarkeit sichtbarkeitXp, + Sichtbarkeit sichtbarkeitLockhistorie) {} + + @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()); + LOGGER.debug("User {} hat Profilbild aktualisiert", user.get().getUserId()); + return ResponseEntity.ok().build(); + } + + @PutMapping("/me/profile") + public ResponseEntity updateProfile(@RequestBody ProfileRequest request, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + var user = userOpt.get(); + if (request.beschreibung() != null && request.beschreibung().length() > 600) { + return ResponseEntity.badRequest().build(); + } + user.setGroesse(request.groesse()); + user.setGewicht(request.gewicht()); + user.setGeschlecht(request.geschlecht()); + user.setNeigung(request.neigung()); + user.setBeziehungsstatus(request.beziehungsstatus()); + user.setBeschreibung(request.beschreibung()); + userRepository.save(user); + LOGGER.info("User {} hat Profil aktualisiert", user.getUserId()); + return ResponseEntity.ok().build(); + } + + @PutMapping("/me/privacy") + public ResponseEntity updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + var user = userOpt.get(); + if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten()); + if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie()); + if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde()); + if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed()); + if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand()); + if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp()); + if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie()); + userRepository.save(user); + LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId()); + return ResponseEntity.ok().build(); + } + + record NotificationPreferenceRequest(boolean inApp, boolean email) {} + + @GetMapping("/me/notifications") + public ResponseEntity> getNotifications(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + Map byKey = notificationPreferenceRepository.findByUserId(userId) + .stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p)); + + Map result = new LinkedHashMap<>(); + for (MessageCause cause : MessageCause.values()) { + NotificationPreferenceEntity pref = byKey.getOrDefault( + cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause)); + Map entry = new LinkedHashMap<>(); + entry.put("inApp", pref.isInApp()); + entry.put("email", pref.isEmail()); + result.put(cause.name(), entry); + } + return ResponseEntity.ok(result); + } + + @PutMapping("/me/notifications") + public ResponseEntity updateNotifications(@RequestBody Map request, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + for (var entry : request.entrySet()) { + MessageCause cause; + try { + cause = MessageCause.valueOf(entry.getKey()); + } catch (IllegalArgumentException e) { + continue; + } + NotificationPreferenceEntity pref = notificationPreferenceRepository + .findByUserIdAndCause(userId, cause) + .orElseGet(() -> { + NotificationPreferenceEntity n = new NotificationPreferenceEntity(); + n.setUserId(userId); + n.setCause(cause); + return n; + }); + pref.setInApp(entry.getValue().inApp()); + pref.setEmail(entry.getValue().email()); + notificationPreferenceRepository.save(pref); + } + return ResponseEntity.ok().build(); + } + + record BdsmDefaultsRequest(List spieltMit, List rollen, List werkzeuge) {} + + @GetMapping("/me/bdsm-defaults") + public ResponseEntity> getBdsmDefaults(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) + .orElse(new BdsmDefaultsEntity()); + Map result = new java.util.LinkedHashMap<>(); + result.put("geschlecht", userOpt.get().getGeschlecht() != null ? userOpt.get().getGeschlecht().name() : null); + result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); + result.put("rollen", splitOrEmpty(d.getRollen())); + result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); + return ResponseEntity.ok(result); + } + + @GetMapping("/{userId}/bdsm-defaults") + public ResponseEntity> getBdsmDefaultsForUser(@PathVariable UUID userId) { + var userOpt = userRepository.findById(userId); + if (userOpt.isEmpty()) return ResponseEntity.notFound().build(); + UserEntity user = userOpt.get(); + BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) + .orElse(new BdsmDefaultsEntity()); + Map result = new java.util.LinkedHashMap<>(); + result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null); + result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); + result.put("rollen", splitOrEmpty(d.getRollen())); + result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); + return ResponseEntity.ok(result); + } + + @PutMapping("/me/bdsm-defaults") + public ResponseEntity updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) + .orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; }); + d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit())); + d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen())); + d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge())); + bdsmDefaultsRepository.save(d); + return ResponseEntity.ok().build(); + } + + private static List splitOrEmpty(String s) { + if (s == null || s.isBlank()) return List.of(); + return List.of(s.split(",")); + } + + @PutMapping("/me/geburtsdatum") + public ResponseEntity updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) { + if (request.geburtsdatum() == null + || Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) { + return ResponseEntity.status(422).build(); + } + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + var user = userOpt.get(); + user.setGeburtsdatum(request.geburtsdatum()); + userRepository.save(user); + LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId()); + 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()); + LOGGER.info("User {} hat Namen zu '{}' geändert", user.get().getUserId(), newName); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/me") + public ResponseEntity deleteAccount(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + String email = userOpt.get().getEmail(); + + userService.deleteAccount(userId, email); + + ResponseCookie cookie = ResponseCookie.from("jwt", "") + .httpOnly(true) + .sameSite("Strict") + .path("/") + .maxAge(0) + .build(); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .build(); + } + + @PostMapping + public ResponseEntity userAnlegen(@RequestBody Registration registration) { + try { + userService.createUser(registration); + return ResponseEntity.status(201).build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } catch (IllegalStateException e) { + return ResponseEntity.status(409).build(); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + return ResponseEntity.internalServerError().build(); + } + } +} 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 8d01c4b..cd27e2d 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserRepository.java @@ -8,7 +8,6 @@ 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/java/de/oaa/xxx/user/UserService.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserService.java new file mode 100644 index 0000000..dd0319c --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserService.java @@ -0,0 +1,210 @@ +package de.oaa.xxx.user; + +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.games.bdsm.entity.AktiveSperreEntity; +import de.oaa.xxx.games.bdsm.entity.MitspielerEntity; +import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository; +import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository; +import de.oaa.xxx.games.bdsm.repository.MitspielerRepository; +import de.oaa.xxx.passwordreset.PasswordResetRepository; +import de.oaa.xxx.registration.Registration; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.social.entity.NotificationPreferenceEntity; +import de.oaa.xxx.social.repository.KommentarLikeRepository; +import de.oaa.xxx.social.repository.KommentarRepository; +import de.oaa.xxx.social.repository.NotificationPreferenceRepository; +import de.oaa.xxx.social.repository.PinnwandEintragRepository; +import de.oaa.xxx.social.repository.PinnwandLikeRepository; +import de.oaa.xxx.social.repository.ProfileImageLikeRepository; +import de.oaa.xxx.social.repository.ProfileImageRepository; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +public class UserService { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + 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 BdsmGameRepository 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; + private final PinnwandEintragRepository pinnwandEintragRepository; + private final PinnwandLikeRepository pinnwandLikeRepository; + private final KommentarRepository kommentarRepository; + private final KommentarLikeRepository kommentarLikeRepository; + private final NotificationPreferenceRepository notificationPreferenceRepository; + + public UserService(UserRepository userRepository, + AufgabenGruppeRepository aufgabenGruppeRepository, + AufgabeRepository aufgabeRepository, + StrafeRepository strafeRepository, + SperreRepository sperreRepository, + ToyRepository toyRepository, + FavoritRepository favoritRepository, + GruppenAboRepository gruppenAboRepository, + BdsmGameRepository sessionRepository, + AktiveSperreRepository aktiveSperreRepository, + MitspielerRepository mitspielerRepository, + EmailChangeRepository emailChangeRepository, + PasswordResetRepository passwordResetRepository, + ProfileImageRepository profileImageRepository, + ProfileImageLikeRepository profileImageLikeRepository, + PinnwandEintragRepository pinnwandEintragRepository, + PinnwandLikeRepository pinnwandLikeRepository, + KommentarRepository kommentarRepository, + KommentarLikeRepository kommentarLikeRepository, + NotificationPreferenceRepository notificationPreferenceRepository) { + this.userRepository = userRepository; + 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; + this.pinnwandEintragRepository = pinnwandEintragRepository; + this.pinnwandLikeRepository = pinnwandLikeRepository; + this.kommentarRepository = kommentarRepository; + this.kommentarLikeRepository = kommentarLikeRepository; + this.notificationPreferenceRepository = notificationPreferenceRepository; + } + + /** + * Löscht einen User-Account vollständig inklusive aller abhängigen Daten. + * Gibt die gelöschte E-Mail zurück (wird für Cookie-Clearing im Controller benötigt). + */ + @Transactional + public void deleteAccount(UUID userId, String email) { + var user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User nicht gefunden: " + userId)); + + LOGGER.info("Lösche Konto für User {}", email); + + // 1. AufgabenGruppen und deren Inhalte löschen + 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. Toys löschen + toyRepository.deleteAll(toyRepository.findByUserId(userId)); + + // 3. Eigene Favoriten und Gruppenabos löschen + favoritRepository.deleteAll(favoritRepository.findByUserId(userId)); + gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId)); + + // 4. BDSM-Session mit Mitspieler und AktiveSperre löschen + 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. Pending Tokens löschen + emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete); + passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete); + + // 5b. Profilbilder und Likes löschen + var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId); + for (var img : profileImages) { + profileImageLikeRepository.deleteByImageId(img.getImageId()); + kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId()) + .forEach(k -> { + kommentarLikeRepository.deleteByKommentarId(k.getKommentarId()); + kommentarRepository.delete(k); + }); + } + profileImageRepository.deleteAll(profileImages); + profileImageLikeRepository.deleteByUserId(userId); + + // 5c. Pinnwand-Einträge und Likes/Kommentare löschen + var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId); + for (var e : ownWallEntries) { + pinnwandLikeRepository.deleteByEintragId(e.getEintragId()); + kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId()) + .forEach(k -> { + kommentarLikeRepository.deleteByKommentarId(k.getKommentarId()); + kommentarRepository.delete(k); + }); + } + pinnwandEintragRepository.deleteAll(ownWallEntries); + pinnwandEintragRepository.deleteByAuthorId(userId); + pinnwandLikeRepository.deleteByUserId(userId); + kommentarRepository.deleteByAuthorId(userId); + kommentarLikeRepository.deleteByUserId(userId); + + // 6. User löschen + userRepository.delete(user); + } + + /** + * Legt einen neuen User aus einer bestätigten Registration an + * und erstellt die Standard-Benachrichtigungseinstellungen. + */ + public void createUser(Registration registration) { + if (registration.getEmail() == null || registration.getPassword() == null || registration.getName() == null) { + throw new IllegalArgumentException("E-Mail, Passwort und Name sind Pflichtfelder"); + } + if (userRepository.findByEmail(registration.getEmail()).isPresent()) { + LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail()); + throw new IllegalStateException("E-Mail bereits vorhanden"); + } + + UserEntity entity = new UserEntity(); + entity.setUserId(UUID.randomUUID()); + entity.setEmail(registration.getEmail()); + entity.setName(registration.getName()); + entity.setPassword(registration.getPassword()); + entity.setGeburtsdatum(registration.getGeburtsdatum()); + userRepository.save(entity); + + for (MessageCause cause : MessageCause.values()) { + notificationPreferenceRepository.save( + NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause)); + } + + LOGGER.info("User {} angelegt", entity.getUserId()); + } +} diff --git a/xxxthegame/src/main/resources/static/bdsm-einladung.html b/xxxthegame/src/main/resources/static/bdsm-einladung.html index 6a13f43..88fdfb2 100644 --- a/xxxthegame/src/main/resources/static/bdsm-einladung.html +++ b/xxxthegame/src/main/resources/static/bdsm-einladung.html @@ -98,7 +98,7 @@ document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.'; document.getElementById('actions').innerHTML = ''; } else if (mode === 'OWN_DEVICE') { - window.location.replace(`/bdsmwarten.html?id=${einladungId}`); + window.location.replace(`/neubdsm.html`); } else { zeigeBestaetigt(); } diff --git a/xxxthegame/src/main/resources/static/bdsm.html b/xxxthegame/src/main/resources/static/bdsm.html index 78d5ada..d4c7de4 100644 --- a/xxxthegame/src/main/resources/static/bdsm.html +++ b/xxxthegame/src/main/resources/static/bdsm.html @@ -1,357 +1,11 @@ - - - - - - - BDSM Game – Neue Session – XXX The Game - - - - - - - - -
-
- -

BDSM Game

-

Schritt 1 von 4 – Session-Einstellungen

- -
-

Session-Einstellungen

- -
-
- - 15 % -
- -
- -
-
- - 15 % -
- -
- - - -
-
- - 5 -
- -
- -
-
- - 1,0 -
- -
-
- -
- - -
-
- - - - - + + + + + + BDSM Game + + + + + diff --git a/xxxthegame/src/main/resources/static/bdsmingame.html b/xxxthegame/src/main/resources/static/bdsmingame.html index 5025d03..db9215f 100644 --- a/xxxthegame/src/main/resources/static/bdsmingame.html +++ b/xxxthegame/src/main/resources/static/bdsmingame.html @@ -337,7 +337,7 @@ const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null'); const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]'); const sessionId = sessionStorage.getItem('bdsm-session-id'); - if (!sessionId) window.location.replace('/bdsm.html'); + if (!sessionId) window.location.replace('/neubdsm.html'); // Multi-Device: bin ich Gast? const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true'; @@ -424,7 +424,7 @@ try { const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`); if (res.status === 204) { zeigeFinaleDialog(); return; } - if (res.status === 400) { window.location.replace('/bdsmtoys.html'); return; } + if (res.status === 400) { window.location.replace('/neubdsm.html'); return; } if (!res.ok) throw new Error(`HTTP ${res.status}`); currentTask = await res.json(); await saveAktiveAufgabe(currentTask, null); diff --git a/xxxthegame/src/main/resources/static/bdsmplayers.html b/xxxthegame/src/main/resources/static/bdsmplayers.html index eccca01..d4c7de4 100644 --- a/xxxthegame/src/main/resources/static/bdsmplayers.html +++ b/xxxthegame/src/main/resources/static/bdsmplayers.html @@ -2,984 +2,10 @@ - - - BDSM Game – Mitspieler – XXX The Game - - - + + BDSM Game - - - - - - -
-
- -

BDSM Game

-

Schritt 2 von 4 – Mitspieler

- -
-

Mitspieler

-
- -
- -
-
- - -
- -
-
- - - + + diff --git a/xxxthegame/src/main/resources/static/bdsmtasks.html b/xxxthegame/src/main/resources/static/bdsmtasks.html index ef3123d..d4c7de4 100644 --- a/xxxthegame/src/main/resources/static/bdsmtasks.html +++ b/xxxthegame/src/main/resources/static/bdsmtasks.html @@ -1,352 +1,11 @@ - - - - - - - BDSM Game – Aufgaben-Gruppen – XXX The Game - - - - - -
-
- -

BDSM Game

-

Schritt 3 von 4 – Aufgaben

- -
-

-
    -
    - -
    -

    -
      -
      - -
      -

      -
        -
        - -
        -
        -
        - - -
        -
        - -
        -
        - - - - - + + + + + + BDSM Game + + + + + diff --git a/xxxthegame/src/main/resources/static/bdsmtoys.html b/xxxthegame/src/main/resources/static/bdsmtoys.html index 45e2f81..d4c7de4 100644 --- a/xxxthegame/src/main/resources/static/bdsmtoys.html +++ b/xxxthegame/src/main/resources/static/bdsmtoys.html @@ -1,372 +1,11 @@ - - - - - - - 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. -

        -
        -
        - -
        -
        -
        - - -
        -
        - -
        -
        - - - - - + + + + + + BDSM Game + + + + + diff --git a/xxxthegame/src/main/resources/static/bdsmwarten.html b/xxxthegame/src/main/resources/static/bdsmwarten.html index de5f8d0..d4c7de4 100644 --- a/xxxthegame/src/main/resources/static/bdsmwarten.html +++ b/xxxthegame/src/main/resources/static/bdsmwarten.html @@ -2,346 +2,10 @@ - - - BDSM Game – Warten – XXX The Game - - - + + BDSM Game - -
        - - - - - -
        - - + + diff --git a/xxxthegame/src/main/resources/static/einladungen.html b/xxxthegame/src/main/resources/static/einladungen.html index 5cef04e..951a101 100644 --- a/xxxthegame/src/main/resources/static/einladungen.html +++ b/xxxthegame/src/main/resources/static/einladungen.html @@ -837,7 +837,7 @@ closeBdsmInviteDialog(); removeRecvItem(key); if (mode === 'OWN_DEVICE') { - window.location.href = `/bdsmwarten.html?id=${key}`; + window.location.href = `/neubdsm.html`; } } catch (_) { errEl.textContent = 'Fehler beim Speichern der Antwort.'; diff --git a/xxxthegame/src/main/resources/static/js/sidebar.js b/xxxthegame/src/main/resources/static/js/sidebar.js index 18cbca6..9669763 100644 --- a/xxxthegame/src/main/resources/static/js/sidebar.js +++ b/xxxthegame/src/main/resources/static/js/sidebar.js @@ -13,7 +13,7 @@ label: 'BDSM Game', icon: '◆', items: [ - { href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' }, + { href: '/neubdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' }, { href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' }, { href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' }, { href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' }, @@ -118,7 +118,7 @@ navAktiv.style.display = ''; const ziel = aktiv.sessionId ? '/bdsmingame.html' - : `/bdsmwarten.html?id=${aktiv.einladungId}`; + : `/neubdsm.html`; navAktiv.querySelector('a').href = ziel; } } else { diff --git a/xxxthegame/src/main/resources/static/login.html b/xxxthegame/src/main/resources/static/login.html index 41c8505..1d28bf2 100644 --- a/xxxthegame/src/main/resources/static/login.html +++ b/xxxthegame/src/main/resources/static/login.html @@ -50,14 +50,6 @@ if (e.key === 'Enter') login(); }); - async function sha256(text) { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - } - async function login() { const email = document.getElementById('email').value.trim(); const password = document.getElementById('password').value; @@ -73,9 +65,11 @@ hideMessage(); try { - const hash = await sha256(password); - const url = `/login?email=${encodeURIComponent(email)}&hash=${encodeURIComponent(hash)}`; - const response = await fetch(url, { method: 'GET' }); + const response = await fetch('/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); if (response.status === 200) { const user = await response.json(); diff --git a/xxxthegame/src/main/resources/static/neubdsm.html b/xxxthegame/src/main/resources/static/neubdsm.html new file mode 100644 index 0000000..45ed8a5 --- /dev/null +++ b/xxxthegame/src/main/resources/static/neubdsm.html @@ -0,0 +1,1484 @@ + + + + + + + BDSM Game – Session einrichten – XXX The Game + + + + + + + + + + + + + + + + + + diff --git a/xxxthegame/src/main/resources/static/registration.html b/xxxthegame/src/main/resources/static/registration.html index de4c9df..7d43f14 100644 --- a/xxxthegame/src/main/resources/static/registration.html +++ b/xxxthegame/src/main/resources/static/registration.html @@ -42,14 +42,6 @@ if (e.key === 'Enter') register(); }); - async function sha256(text) { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - } - async function register() { const name = document.getElementById('name').value.trim(); const email = document.getElementById('email').value.trim(); @@ -84,11 +76,10 @@ hideMessage(); try { - const passwordHash = await sha256(password); const response = await fetch('/registration', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, passwordHash, geburtsdatum }) + body: JSON.stringify({ name, email, password, geburtsdatum }) }); if (response.status === 202) { diff --git a/xxxthegame/src/main/resources/static/reset-password.html b/xxxthegame/src/main/resources/static/reset-password.html index 2669aba..ebf91ff 100644 --- a/xxxthegame/src/main/resources/static/reset-password.html +++ b/xxxthegame/src/main/resources/static/reset-password.html @@ -78,14 +78,6 @@ } }); - async function sha256(text) { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - } - async function submit() { const password = document.getElementById('password').value; const passwordConfirm = document.getElementById('passwordConfirm').value; @@ -110,11 +102,10 @@ hideMessage(); try { - const passwordHash = await sha256(password); const response = await fetch('/password-reset/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token, passwordHash }) + body: JSON.stringify({ token, password }) }); if (response.ok) { diff --git a/xxxthegame/src/main/resources/static/userhome.html b/xxxthegame/src/main/resources/static/userhome.html index 9c399f5..b5a54c8 100644 --- a/xxxthegame/src/main/resources/static/userhome.html +++ b/xxxthegame/src/main/resources/static/userhome.html @@ -54,7 +54,7 @@ Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen. Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.

        - +