Weiter an der Konfig Maske für das BDSM Game gearbeitet, Refactoring der Controller Klassen

This commit is contained in:
2026-03-20 16:05:25 +01:00
parent dc0a3f6e85
commit 173302e0ee
52 changed files with 7060 additions and 6825 deletions

View File

@@ -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* \\\\\\))"
]
}
}

View File

@@ -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

View File

@@ -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.<init>(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.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(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.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(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.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(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.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(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.<init>(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.<init>(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.<init>(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.<init>(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.<init>(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.<init>(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.<init>(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.<init>(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

View File

@@ -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,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -2,4 +2,6 @@
<typeInfoHistroy>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{AboController.java[AboController" modifiers="1" timestamp="1773400404000"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.history{GameHistoryEntity.java[GameHistoryEntity" modifiers="1" timestamp="1773860770365"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.bdsm.controller{BdsmGameController.java[BdsmGameController" modifiers="1" timestamp="1774017499554"/>
<typeInfo handle="=xxxthegame/\/usr\/lib\/jvm\/java-21-openjdk-amd64\/lib\/jrt-fs.jar`java.base=/javadoc_location=/https:\/\/docs.oracle.com\/en\/java\/javase\/21\/docs\/api\/=/&lt;java.util(UUID.class[UUID" modifiers="49" timestamp="1769125611000"/>
</typeInfoHistroy>

View File

@@ -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.

View File

@@ -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

BIN
bilder/logo_dating.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View File

@@ -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<ToyEntity> 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<UUID, ToyEntity> 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<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
if (source == null || source.isEmpty()) return new ArrayList<>();
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
}
}

View File

@@ -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<Void> 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<ToyEntity> 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<UUID, ToyEntity> 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<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
if (source == null || source.isEmpty()) return new ArrayList<>();
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
}
// ── Löschen ──

View File

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

View File

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

View File

@@ -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<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
public ResponseEntity<Map<String, Object>> 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);

View File

@@ -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<UUID> 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<UUID> 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<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
@@ -159,46 +161,8 @@ public class BdsmGameController {
public ResponseEntity<Void> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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. */

View File

@@ -33,10 +33,15 @@ public class BdsmSetupDraftController {
}
@GetMapping
public ResponseEntity<Map<String, Object>> getDraft(Principal principal) {
public ResponseEntity<Map<String, Object>> 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<String, Object> m = new LinkedHashMap<>();
m.put("setupId", d.getSetupId());

View File

@@ -8,4 +8,5 @@ import java.util.UUID;
public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> {
Optional<BdsmSetupDraftEntity> findByUserId(UUID userId);
Optional<BdsmSetupDraftEntity> findBySetupId(String setupId);
}

View File

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

View File

@@ -1,3 +1,3 @@
package de.oaa.xxx.passwordreset;
public record PasswordResetConfirm(String token, String passwordHash) {}
public record PasswordResetConfirm(String token, String password) {}

View File

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

View File

@@ -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<Void> activate(@PathVariable String uuid) {
RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null);
if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) {
ResponseEntity<Void> 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<Void> 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();
}
}
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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<User> login(@RequestParam String email, @RequestParam String hash,
HttpServletResponse response) {
Optional<UserEntity> 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<User> 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<User> 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<User> 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<User> 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<User> get(@PathVariable UUID userId) {
return userRepository.findById(userId)
.map(entity -> ResponseEntity.ok(entity.toUser()))
.orElse(ResponseEntity.noContent().build());
}
}

View File

@@ -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<Void> 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<Void> 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<Void> 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<Map<String, Object>> getNotifications(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
Map<String, Object> result = new LinkedHashMap<>();
for (MessageCause cause : MessageCause.values()) {
NotificationPreferenceEntity pref = byKey.getOrDefault(
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
Map<String, Object> 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<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> 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<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
@GetMapping("/me/bdsm-defaults")
public ResponseEntity<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<Void> 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<String> splitOrEmpty(String s) {
if (s == null || s.isBlank()) return List.of();
return List.of(s.split(","));
}
@PutMapping("/me/geburtsdatum")
public ResponseEntity<Void> 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<Void> 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<Void> 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<AktiveSperreEntity> sperren = session.getAktiveSperren();
List<MitspielerEntity> 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<Void> 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<Void> 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<Void> 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<Void> 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<Map<String, Object>> getNotifications(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
Map<String, Object> result = new LinkedHashMap<>();
for (MessageCause cause : MessageCause.values()) {
NotificationPreferenceEntity pref = byKey.getOrDefault(
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
Map<String, Object> 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<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> 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<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
@GetMapping("/me/bdsm-defaults")
public ResponseEntity<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<Void> 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<String> splitOrEmpty(String s) {
if (s == null || s.isBlank()) return List.of();
return List.of(s.split(","));
}
@PutMapping("/me/geburtsdatum")
public ResponseEntity<Void> 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<Void> 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<Void> 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<Void> 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();
}
}
}

View File

@@ -8,7 +8,6 @@ import java.util.UUID;
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmailAndPassword(String email, String password);
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByName(String name);
List<UserEntity> findByNameContainingIgnoreCase(String name);

View File

@@ -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<AktiveSperreEntity> sperren = session.getAktiveSperren();
List<MitspielerEntity> 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());
}
}

View File

@@ -98,7 +98,7 @@
document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
} else if (mode === 'OWN_DEVICE') {
window.location.replace(`/bdsmwarten.html?id=${einladungId}`);
window.location.replace(`/neubdsm.html`);
} else {
zeigeBestaetigt();
}

View File

@@ -1,357 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Neue Session XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.setting-row { margin-bottom: 1.25rem; }
.setting-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.4rem;
}
.setting-header label {
font-size: 0.85rem;
color: #aaa;
margin: 0;
}
.setting-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-primary);
min-width: 3.5rem;
text-align: right;
}
input[type="range"] {
width: 100%;
accent-color: var(--color-primary);
cursor: pointer;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem;
max-width: 420px;
width: 100%;
text-align: center;
}
.modal-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.75rem;
}
.modal-text {
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.modal-actions { display: flex; flex-direction: column; gap: 0.6rem; }
.modal-actions button { width: 100%; padding: 0.75rem; }
</style>
</head>
<body class="app">
<div class="modal-overlay" id="modal" style="display:none;">
<div class="modal-card">
<div class="modal-title" id="modalTitle"></div>
<div class="modal-text" id="modalText"></div>
<div class="modal-actions" id="modalActions"></div>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 1 von 4 Session-Einstellungen</p>
<div class="setup-section">
<h2>Session-Einstellungen</h2>
<div class="setting-row">
<div class="setting-header">
<label for="sldStrafe">Wahrscheinlichkeit Strafe</label>
<span class="setting-value"><span id="valStrafe">15</span> %</span>
</div>
<input type="range" id="sldStrafe" min="0" max="100" value="15"
oninput="document.getElementById('valStrafe').textContent=this.value; updateWarnung()">
</div>
<div class="setting-row">
<div class="setting-header">
<label for="sldZeitstrafe">Wahrscheinlichkeit Zeitstrafe</label>
<span class="setting-value"><span id="valZeitstrafe">15</span> %</span>
</div>
<input type="range" id="sldZeitstrafe" min="0" max="100" value="15"
oninput="document.getElementById('valZeitstrafe').textContent=this.value; updateWarnung()">
</div>
<div class="message" id="wahrschWarnung" style="display:none; margin-top:0.75rem;"></div>
<div class="setting-row">
<div class="setting-header">
<label for="sldAufgaben">Aufgaben pro Level</label>
<span class="setting-value" id="valAufgaben">5</span>
</div>
<input type="range" id="sldAufgaben" min="1" max="20" value="5"
oninput="document.getElementById('valAufgaben').textContent=this.value">
</div>
<div class="setting-row">
<div class="setting-header">
<label for="sldZeit">Zeitfaktor Zeitstrafen</label>
<span class="setting-value" id="valZeit">1,0</span>
</div>
<input type="range" id="sldZeit" min="5" max="20" value="10"
oninput="document.getElementById('valZeit').textContent=(this.value/10).toFixed(1).replace('.',',')">
</div>
</div>
<div class="message" id="message"></div>
<button class="full-width" onclick="weiter()">Weiter</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
function updateWarnung() {
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
const summe = strafe + zeitstrafe;
const el = document.getElementById('wahrschWarnung');
if (summe > 98) {
el.textContent = `Kombiniert ${summe} % Werte über 98 % sind nicht möglich.`;
el.className = 'message error';
el.style.display = 'block';
} else if (summe > 60) {
el.textContent = `Hinweis: Bei ${summe} % kombinierten Wahrscheinlichkeiten ist die Chance auf Vanilla-Aufgaben sehr gering.`;
el.className = 'message warning';
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
async function weiter() {
hideMessage();
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
if (strafe + zeitstrafe > 98) {
showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error');
return;
}
const settings = {
wahrscheinlichkeitStrafe: strafe,
wahrscheinlichkeitSperre: zeitstrafe,
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
};
// Immer neue Setup-ID → Mitspieler-Konfig und Einladungen gehören zur neuen Runde
const newSetupId = crypto.randomUUID();
sessionStorage.setItem('bdsm-setup-id', newSetupId);
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(settings));
sessionStorage.removeItem('bdsm-session-setup');
sessionStorage.removeItem('bdsm-session-gruppen');
sessionStorage.removeItem('bdsm-session-toys');
// Alten Draft löschen, neuen mit frischer Setup-ID anlegen
try { await fetch('/bdsm/setup-draft', { method: 'DELETE' }); } catch (_) {}
fetch('/bdsm/setup-draft', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId: newSetupId, settingsJson: JSON.stringify(settings) }),
}).catch(() => {});
window.location.href = '/bdsmplayers.html';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
// ── Aktive-Session-Check ──
function zeigeModal(title, text, actions) {
document.getElementById('modalTitle').textContent = title;
const textEl = document.getElementById('modalText');
textEl.textContent = text;
textEl.style.display = text ? '' : 'none';
const actEl = document.getElementById('modalActions');
actEl.innerHTML = '';
actions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.className = a.primary ? 'full-width' : 'full-width secondary';
btn.onclick = () => a.onClick();
actEl.appendChild(btn);
});
document.getElementById('modal').style.display = 'flex';
}
function versteckeModal() {
document.getElementById('modal').style.display = 'none';
}
const BDSM_STORAGE_KEYS = [
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
];
function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid);
window.location.href = '/bdsmingame.html';
}
function sessionBeendenFragen(sid) {
zeigeModal(
'Session wirklich beenden?',
'Die Session und alle aktiven Sperren werden gelöscht.',
[
{ label: 'Ja, beenden', primary: true, onClick: () => sessionLoeschen(sid) },
{ label: 'Nein, fortfahren', onClick: () => sessionFortfahren(sid) },
]
);
}
async function sessionLoeschen(sid) {
versteckeModal();
try {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }),
});
} catch (_) { /* ignorieren */ }
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
}
(async function checkAktiveSession() {
try {
const meRes = await fetch('/login/me');
if (!meRes.ok) return;
const user = await meRes.json();
// 1. Prüfen ob User selbst eine aktive Host-Session hat
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
if (sessionRes.ok) {
const session = await sessionRes.json();
zeigeModal(
'Aktive Session vorhanden',
'Du hast noch eine laufende Session. Möchtest du fortfahren?',
[
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
]
);
return;
}
// 2. Prüfen ob User als Mitspieler (ACCEPTED_OWN) eingeladen wurde
const einladungRes = await fetch('/bdsm/einladung/meine-aktive');
if (!einladungRes.ok) return;
const einladung = await einladungRes.json();
if (!einladung.sessionId) {
// Spiel noch nicht gestartet → Warteseite
window.location.replace(`/bdsmwarten.html?id=${einladung.einladungId}`);
} else {
// Spiel läuft bereits → direkt als Gast rein
const mRes = await fetch(`/bdsm/${einladung.sessionId}/mitspieler/me`);
if (mRes.ok) {
const mData = await mRes.json();
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
sessionStorage.setItem('bdsm-guest-name', mData.name);
}
sessionStorage.setItem('bdsm-session-id', einladung.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
window.location.replace('/bdsmingame.html');
}
} catch (_) { /* ignorieren */ }
})();
// Gespeicherte Einstellungen wiederherstellen
function applySettings(s) {
function setSlider(id, displayId, value, transform) {
const el = document.getElementById(id);
if (!el) return;
el.value = value;
document.getElementById(displayId).textContent = transform ? transform(value) : value;
}
setSlider('sldStrafe', 'valStrafe', s.wahrscheinlichkeitStrafe);
setSlider('sldZeitstrafe', 'valZeitstrafe', s.wahrscheinlichkeitSperre);
setSlider('sldAufgaben', 'valAufgaben', s.aufgabenProLevel);
setSlider('sldZeit', 'valZeit', Math.round(s.zeitfaktorZeitstrafen * 10),
v => (v / 10).toFixed(1).replace('.', ','));
updateWarnung();
}
(async function restore() {
const saved = sessionStorage.getItem('bdsm-session-settings');
if (saved) {
applySettings(JSON.parse(saved));
return;
}
// Fallback: Draft aus DB laden
try {
const res = await fetch('/bdsm/setup-draft');
if (!res.ok) return;
const draft = await res.json();
if (draft.setupId && !sessionStorage.getItem('bdsm-setup-id')) {
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
}
if (draft.settingsJson) {
const s = JSON.parse(draft.settingsJson);
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(s));
applySettings(s);
}
if (draft.setupJson) {
sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
}
if (draft.gruppenJson) {
sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
}
} catch (_) {}
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title>
</head>
<body>
<script>window.location.replace('/neubdsm.html');</script>
</body>
</html>

View File

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

View File

@@ -2,984 +2,10 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Mitspieler XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Player cards ── */
.player-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.player-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.player-title {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
}
.player-badge {
background: var(--color-primary);
color: #fff;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-pending {
background: var(--color-secondary);
color: var(--color-muted);
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-accepted {
background: #1a5c2a;
color: #6fcf97;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-remove {
margin-left: auto;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.player-remove:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: transparent;
}
.btn-invite {
background: transparent;
border: 1px solid var(--color-primary);
color: var(--color-primary);
padding: 0.45rem 1rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-invite:hover { background: var(--color-primary); color: #fff; }
.btn-cancel-invite {
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
}
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
.pending-info {
text-align: center;
color: var(--color-muted);
font-size: 0.9rem;
padding: 1.5rem 0;
}
.pending-name {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
margin-bottom: 0.25rem;
}
.pending-mode {
font-size: 0.8rem;
color: var(--color-muted);
}
.card-field { margin-bottom: 1rem; }
.card-field > label {
font-size: 0.8rem;
color: #aaa;
margin: 0 0 0.5rem 0;
display: block;
}
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item {
display: inline-flex;
align-items: flex-start;
gap: 0.45rem;
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 6px;
padding: 0.4rem 0.7rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.check-item.is-checked { border-color: var(--color-primary); }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.add-player-btn {
width: 100%;
background: transparent;
border: 1px dashed var(--color-secondary);
color: var(--color-muted);
padding: 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: normal;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
margin-bottom: 2rem;
}
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
/* ── Freunde-Modal ── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.75);
z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.75rem;
max-width: 420px; width: 100%;
}
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 1rem; }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
.friend-combobox { position: relative; }
.friend-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem;
}
.friend-dropdown-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s;
font-size: 0.9rem; font-weight: 600;
}
.friend-dropdown-item:hover { background: var(--color-secondary); }
.selected-friend-box {
display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem;
background: var(--color-secondary); border-radius: 8px;
font-size: 0.9rem; font-weight: 600;
border: 1px solid var(--color-primary); color: var(--color-text);
}
.modal-cancel { margin-top: 0.6rem; width: 100%; }
</style>
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title>
</head>
<body class="app">
<div class="modal-overlay" id="errorModal" style="display:none;">
<div class="modal-card" style="text-align:center;">
<div style="font-size:1.5rem;margin-bottom:0.75rem;">⚠️</div>
<div class="modal-title" id="errorModalTitle"></div>
<div class="modal-text" id="errorModalText" style="font-size:0.9rem;color:var(--color-muted);line-height:1.5;margin-bottom:1.25rem;"></div>
<button onclick="document.getElementById('errorModal').style.display='none'">OK</button>
</div>
</div>
<div class="modal-overlay" id="friendModal" style="display:none;">
<div class="modal-card">
<div class="modal-title">Freund einladen</div>
<div class="friend-combobox">
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
<div class="friend-dropdown" id="friendDropdown"></div>
</div>
<div class="selected-friend-box" id="selectedFriendBox"></div>
<button id="btnEinladen" style="margin-top:1rem; width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
<button class="secondary modal-cancel" onclick="schliesseFriendModal()">Abbrechen</button>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 2 von 4 Mitspieler</p>
<div class="setup-section">
<h2>Mitspieler</h2>
<div id="playersContainer"></div>
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
<div class="message" id="message"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsm.html'">← Zurück</button>
<button style="flex:2;" id="weiterBtn" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
// SetupId erzeugen (persistent über sessionStorage)
if (!sessionStorage.getItem('bdsm-setup-id')) {
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
}
let setupId = sessionStorage.getItem('bdsm-setup-id');
// Draft aus DB laden wenn sessionStorage leer
async function ladeSessionOderDraft() {
if (sessionStorage.getItem('bdsm-session-settings')) return true;
try {
const res = await fetch('/bdsm/setup-draft');
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
const draft = await res.json();
if (draft.setupId) {
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
setupId = draft.setupId;
}
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
if (!draft.settingsJson) { window.location.replace('/bdsm.html'); return false; }
return true;
} catch (_) {
window.location.replace('/bdsm.html');
return false;
}
}
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
let playerSeq = 0;
let playerIds = [];
// { [playerId]: { einladungId, status, inviteeId, inviteeName, mode } | null }
let playerInvitations = {};
let pollIntervalId = null;
let myUserId = null;
let selfPlayerId = null;
let freundeListe = [];
function buildCheckItems(name, items, type, disabled = false) {
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function createCardHtml(id, prefillName, isSelf) {
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
const num = playerIds.indexOf(id) + 1;
const nameField = isSelf
? `<input type="text" id="p${id}-name" value="${prefillName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
: `<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">`;
const inviteBtn = isSelf ? '' : `<button class="btn-invite" onclick="oeffneFreundeModal(${id})">👥 Einladen</button>`;
return `
<div class="player-card" id="player-${id}">
<div class="player-card-header">
<span class="player-title">Spieler ${num}</span>
${badge}
${inviteBtn}
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
</div>
<div id="p${id}-body">
${buildPlayerBody(id, nameField, isSelf)}
</div>
</div>`;
}
function buildPlayerBody(id, nameField, genderDisabled = false) {
return `
<div class="card-field">
<label>Name</label>
${nameField}
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio', genderDisabled)}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
<div id="p${id}-chastity-hint" style="display:none;font-size:0.78rem;color:var(--color-muted);margin-top:0.4rem;font-style:italic;line-height:1.4;"></div>
</div>
<div class="card-field">
<label>Finale</label>
<label class="check-item is-checked" id="p${id}-sperre-label">
<input type="checkbox" id="p${id}-sperrenAufloesen" checked onchange="toggleSperreWarning(${id})">
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
</label>
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="p${id}-sperre-warn">
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Diese Person könnte im Finale leer ausgehen.
</div>
</div>`;
}
function addPlayer(prefillName = '', isSelf = false) {
playerSeq++;
const id = playerSeq;
if (isSelf) selfPlayerId = id;
playerIds.push(id);
playerInvitations[id] = null;
document.getElementById('playersContainer')
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
refreshRemoveButtons();
return id;
}
function removePlayer(id) {
const inv = playerInvitations[id];
// Einladung serverseitig canceln
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
}
playerInvitations[id] = null;
if (playerIds.length <= 2) {
// Letzter Slot: nur leeren, nicht entfernen
const header = document.querySelector(`#player-${id} .player-card-header`);
if (header) {
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const invBtn = header.querySelector('.btn-invite');
if (invBtn) invBtn.style.display = '';
}
const body = document.getElementById(`p${id}-body`);
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
return;
}
document.getElementById('player-' + id)?.remove();
playerIds = playerIds.filter(x => x !== id);
delete playerInvitations[id];
refreshPlayerTitles();
refreshRemoveButtons();
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const el = document.querySelector(`#player-${id} .player-title`);
if (el) el.textContent = 'Spieler ' + (idx + 1);
});
}
function refreshRemoveButtons() {
playerIds.forEach((id, idx) => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = idx === 0 ? 'none' : '';
});
}
document.addEventListener('change', e => {
const input = e.target;
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r => {
r.closest('.check-item')?.classList.toggle('is-checked', r.checked);
});
if (input.checked && input.name.endsWith('-geschlecht')) {
const prefix = input.name.slice(0, -'-geschlecht'.length);
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
if (cb.closest('.check-item')?.dataset.chastitylocked) return;
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
const label = input.closest('.check-item');
if (label?.dataset.chastitylocked) {
// Revert immediately and flash hint
input.checked = false;
label.classList.remove('is-checked');
const card = label.closest('[id^="player-"]');
if (card) flashChastityHint(card.id.replace('player-', ''));
return;
}
label?.classList.toggle('is-checked', input.checked);
}
});
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function toggleSperreWarning(id) {
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
const warn = document.getElementById(`p${id}-sperre-warn`);
const label = document.getElementById(`p${id}-sperre-label`);
if (!cb) return;
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
if (label) label.classList.toggle('is-checked', cb.checked);
}
function setFieldError(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'block' : 'none';
}
// ── Freunde-Modal ──
let currentInvitePlayerId = null;
let selectedFriend = null; // { userId, name }
async function oeffneFreundeModal(playerId) {
currentInvitePlayerId = playerId;
selectedFriend = null;
document.getElementById('friendSearch').value = '';
document.getElementById('friendDropdown').style.display = 'none';
document.getElementById('friendDropdown').innerHTML = '';
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('selectedFriendBox').textContent = '';
document.getElementById('btnEinladen').disabled = true;
document.getElementById('friendModal').style.display = 'flex';
if (freundeListe.length === 0) {
try {
const res = await fetch('/social/friends');
freundeListe = res.ok ? await res.json() : [];
} catch (_) { freundeListe = []; }
}
}
function filterFreunde(query) {
selectedFriend = null;
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
const dropdown = document.getElementById('friendDropdown');
dropdown.innerHTML = '';
const q = query.trim().toLowerCase();
if (!q) { dropdown.style.display = 'none'; return; }
const invitedIds = new Set(
Object.values(playerInvitations)
.filter(inv => inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST'))
.map(inv => inv.inviteeId)
);
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q) && !invitedIds.has(f.user.userId));
if (!matches.length) {
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
dropdown.style.display = 'block';
return;
}
matches.forEach(f => {
const item = document.createElement('div');
item.className = 'friend-dropdown-item';
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt'));
if (f.user.profilePicture) {
const img = document.createElement('img');
img.className = 'friend-avatar';
img.src = 'data:image/png;base64,' + f.user.profilePicture;
img.alt = '';
item.appendChild(img);
} else {
const av = document.createElement('div');
av.className = 'friend-avatar';
item.appendChild(av);
}
const span = document.createElement('span');
span.textContent = f.user.name || 'Unbekannt';
item.appendChild(span);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function selectFriend(userId, name) {
selectedFriend = { userId, name };
document.getElementById('friendSearch').value = name;
document.getElementById('friendDropdown').style.display = 'none';
const box = document.getElementById('selectedFriendBox');
box.textContent = '✓ ' + name;
box.style.display = 'block';
document.getElementById('btnEinladen').disabled = false;
}
async function confirmedEinladen() {
if (!selectedFriend) return;
await einladen(selectedFriend.userId, selectedFriend.name);
}
function schliesseFriendModal() {
document.getElementById('friendModal').style.display = 'none';
currentInvitePlayerId = null;
selectedFriend = null;
}
function zeigePopup(title, text) {
document.getElementById('errorModalTitle').textContent = title;
document.getElementById('errorModalText').textContent = text;
document.getElementById('errorModal').style.display = 'flex';
}
async function einladen(inviteeId, inviteeName) {
const id = currentInvitePlayerId;
schliesseFriendModal();
if (!id) return;
try {
const res = await fetch('/bdsm/einladung', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
});
if (res.status === 409) {
zeigePopup('Bereits eingeladen', `${inviteeName} ist bereits eingeladen oder nimmt schon am Spiel teil.`);
return;
}
if (!res.ok) throw new Error();
const data = await res.json();
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
renderPending(id);
startPoll();
} catch (_) {
zeigePopup('Fehler', 'Einladung konnte nicht gesendet werden. Bitte versuche es erneut.');
}
}
function renderPending(id) {
const inv = playerInvitations[id];
if (!inv) return;
const body = document.getElementById(`p${id}-body`);
if (!body) return;
const headerInvBtn = document.querySelector(`#player-${id} .btn-invite`);
if (headerInvBtn) headerInvBtn.style.display = 'none';
if (inv.status === 'PENDING') {
// Badge nach "Spieler X" einfügen
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-pending">Ausstehend</span>`);
body.innerHTML = `
<div class="pending-info">
<div class="pending-name">${inv.inviteeName}</div>
<div>Einladung wurde gesendet warte auf Antwort…</div>
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
</div>`;
} else if (inv.status === 'ACCEPTED_OWN') {
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Eigenes Gerät</span>`);
body.innerHTML = `
<div class="pending-info">
<div class="pending-name">${inv.inviteeName}</div>
<div class="pending-mode">Spieler konfiguriert Präferenzen auf dem eigenen Gerät.</div>
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
</div>`;
} else if (inv.status === 'ACCEPTED_HOST') {
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Host-Gerät</span>`);
const nameField = `<input type="text" id="p${id}-name" value="${inv.inviteeName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`;
const hasGeschlecht = inv.defaults && inv.defaults.geschlecht;
body.innerHTML = buildPlayerBody(id, nameField, hasGeschlecht);
if (inv.defaults) {
restorePlayer(id, {
geschlecht: inv.defaults.geschlecht,
spieltMit: inv.defaults.spieltMit || [],
rollen: inv.defaults.rollen || [],
werkzeuge: inv.defaults.werkzeuge || [],
});
}
pruefeChastityConstraint(id, inv.inviteeId);
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
// Slot wieder freigeben
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
if (headerInvBtn) headerInvBtn.style.display = '';
playerInvitations[id] = null;
body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
}
async function cancelEinladung(id) {
const inv = playerInvitations[id];
if (!inv) return;
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null;
// UI zurücksetzen
const header = document.querySelector(`#player-${id} .player-card-header`);
if (header) {
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const invBtn = header.querySelector('.btn-invite');
if (invBtn) invBtn.style.display = '';
}
const body = document.getElementById(`p${id}-body`);
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
// ── Polling ──
function startPoll() {
if (pollIntervalId) return;
pollIntervalId = setInterval(pollEinladungen, 3000);
}
function stopPoll() {
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
}
async function pollEinladungen() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (!hasPending) { stopPoll(); return; }
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (!res.ok) return;
const liste = await res.json();
for (const e of liste) {
const id = playerIds.find(pid => {
const inv = playerInvitations[pid];
return inv && inv.einladungId === e.einladungId;
});
if (!id) continue;
const inv = playerInvitations[id];
if (!inv || inv.status === e.status) continue;
inv.status = e.status;
if (e.status === 'DECLINED' || e.status === 'CANCELLED') {
showMessage(`${inv.inviteeName} hat die Einladung abgelehnt oder abgebrochen.`, 'error');
}
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
// Profil + Defaults der eingeladenen Person laden
try {
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
if (dRes.ok) inv.defaults = await dRes.json();
} catch (_) {}
}
renderPending(id);
}
} catch (_) {}
updateWeiterBtn();
}
function updateWeiterBtn() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
document.getElementById('weiterBtn').disabled = hasPending;
}
// ── Validation & Weiter ──
function weiter() {
hideMessage();
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (hasPending) {
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error');
return;
}
let valid = true;
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
const mitspieler = playerIds.map(id => {
const inv = playerInvitations[id];
const isOwnDevice = inv && inv.status === 'ACCEPTED_OWN';
if (isOwnDevice) {
// Daten werden vom Spieler selbst auf dem eigenen Gerät konfiguriert
return {
name: inv.inviteeName,
geschlecht: null,
spieltMit: [], rollen: [], werkzeuge: [],
userId: inv.inviteeId,
eigenesGeraet: true,
einladungId: inv.einladungId,
sperrenVorFinaleAufloesen: true, // Wird auf dem eigenen Gerät konfiguriert
};
}
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
valid = false;
}
const sperrenAufloesen = document.getElementById(`p${id}-sperrenAufloesen`);
return {
name,
geschlecht: geschlecht[0] || null,
spieltMit, rollen, werkzeuge,
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
eigenesGeraet: false,
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
};
});
if (!valid) { showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error'); return; }
const configuredMitspieler = mitspieler.filter(p => !p.eigenesGeraet);
const hasOwnDeviceInRoleCheck = mitspieler.some(p => p.eigenesGeraet);
if (!hasOwnDeviceInRoleCheck) {
const allRoles = new Set(configuredMitspieler.flatMap(p => p.rollen));
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
if (missingRoles.length > 0) {
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
return;
}
}
const hasOwnDevicePlayers = mitspieler.some(p => p.eigenesGeraet);
if (!hasOwnDevicePlayers) {
let partnerFehler = false;
configuredMitspieler.forEach((player, i) => {
const andereGeschlechter = configuredMitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
if (!hatPartner) {
const globalIdx = mitspieler.indexOf(player);
setFieldError(`p${playerIds[globalIdx]}-partner-err`, true);
partnerFehler = true;
}
});
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
}
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
const sessionSetup = JSON.stringify({ settings, mitspieler });
sessionStorage.setItem('bdsm-session-setup', sessionSetup);
// Draft in DB aktualisieren
fetch('/bdsm/setup-draft', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
setupId,
setupJson: sessionSetup,
}),
}).catch(() => {});
window.location.href = '/bdsmtasks.html';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text; el.className = `message ${type}`; el.style.display = 'block';
}
function hideMessage() { document.getElementById('message').style.display = 'none'; }
// ── Chastity-Constraint: gesperrtes Werkzeug angehakt + disabled ──
async function pruefeChastityConstraint(playerId, userId) {
if (!userId) return;
try {
const res = await fetch(`/bdsm/chastity-constraint?userId=${userId}`);
if (!res.ok) return;
const { lockedWerkzeug } = await res.json();
if (!lockedWerkzeug) return;
const locked = lockedWerkzeug === 'BOTH' ? ['VAGINA', 'PENIS'] : [lockedWerkzeug];
locked.forEach(w => sperrWerkzeugCheckbox(playerId, w));
// Hinweis-Text im hidden div speichern, Popup beim Klick auf Checkbox
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
if (hintEl) {
hintEl.dataset.hintText = '🔒 Das System weiß aus halbwegs verlässlicher Quelle, dass diese Person dieses Körperteil gerade nicht einsetzen kann und lässt sich hier auch nicht austricksen.';
}
} catch (_) {}
}
function sperrWerkzeugCheckbox(playerId, werkzeug) {
const cb = document.querySelector(`input[name="p${playerId}-werkzeuge"][value="${werkzeug}"]`);
if (!cb) return;
cb.checked = false;
cb.disabled = false;
const label = cb.closest('.check-item');
if (label) {
label.classList.remove('is-checked', 'is-disabled');
label.dataset.chastitylocked = '1';
}
}
function flashChastityHint(playerId) {
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
const text = hintEl?.dataset.hintText;
if (!text) return;
document.getElementById('errorModalTitle').textContent = 'Nicht verfügbar';
document.getElementById('errorModalText').textContent = text;
document.getElementById('errorModal').style.display = 'flex';
}
// Klick auf gesperrte Checkbox: Hint aufblinken lassen
document.addEventListener('click', e => {
const label = e.target.closest('.check-item[data-chastitylocked]');
if (!label) return;
const card = label.closest('[id^="player-"]');
if (!card) return;
const playerId = card.id.replace('player-', '');
flashChastityHint(playerId);
});
function restorePlayer(id, data) {
if (data.geschlecht) {
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.rollen || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.werkzeuge || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
if (data.sperrenVorFinaleAufloesen === false) {
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
if (cb) { cb.checked = false; toggleSperreWarning(id); }
}
}
// ── Einladungen aus DB wiederherstellen ──
async function ladeEinladungenAusDb(userIdToInfo) {
// userIdToInfo: { [userId]: { playerId, name } } oder null (dann Matching per slotIndex)
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (!res.ok) return;
const einladungen = await res.json();
const aktive = einladungen.filter(e =>
e.status === 'PENDING' || e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST');
for (const e of aktive) {
let playerId;
if (userIdToInfo && userIdToInfo[e.inviteeId]) {
playerId = userIdToInfo[e.inviteeId].playerId;
} else {
// Fallback: slotIndex direkt als playerId nutzen (wenn playerIds das enthält)
playerId = playerIds.find(pid => pid === e.slotIndex);
}
if (!playerId) continue;
const inviteeName = (userIdToInfo?.[e.inviteeId]?.name) || e.inviteeName || '';
playerInvitations[playerId] = {
einladungId: e.einladungId,
status: e.status,
inviteeId: e.inviteeId,
inviteeName,
};
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
try {
const dRes = await fetch(`/user/${e.inviteeId}/bdsm-defaults`);
if (dRes.ok) playerInvitations[playerId].defaults = await dRes.json();
} catch (_) {}
}
renderPending(playerId);
}
if (aktive.some(e => e.status === 'PENDING')) startPoll();
updateWeiterBtn();
} catch (_) {}
}
// ── Init ──
async function init() {
const ok = await ladeSessionOderDraft();
if (!ok) return;
// myUserId immer laden (wird für userId des Host-Spielers benötigt)
const user = await fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null);
myUserId = user?.userId || null;
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
if (savedSetup) {
const { mitspieler } = JSON.parse(savedSetup);
const userIdToInfo = {};
mitspieler.forEach((p, i) => {
const id = addPlayer(p.name, i === 0);
restorePlayer(id, p);
if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name };
});
mitspieler.forEach((p, i) => { if (p.userId) pruefeChastityConstraint(playerIds[i], p.userId); });
await ladeEinladungenAusDb(userIdToInfo);
} else {
const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
addPlayer(user ? user.name : '', true);
addPlayer();
const selfId = playerIds[0];
restorePlayer(selfId, {
geschlecht: user?.geschlecht || null,
spieltMit: defaults.spieltMit || [],
rollen: defaults.rollen || [],
werkzeuge: defaults.werkzeuge || [],
});
if (myUserId) pruefeChastityConstraint(selfId, myUserId);
// Auch für frischen Start: evtl. noch offene Einladungen aus vorheriger Session
await ladeEinladungenAusDb(null);
}
}
init();
</script>
<body>
<script>window.location.replace('/neubdsm.html');</script>
</body>
</html>

View File

@@ -1,352 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Aufgaben-Gruppen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.select-all-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.select-all-label input {
accent-color: var(--color-primary);
width: 14px;
height: 14px;
cursor: pointer;
flex-shrink: 0;
}
.gruppe-list { list-style: none; padding: 0; margin: 0; }
.gruppe-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.85rem;
border-radius: 8px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.gruppe-item.is-checked { border-color: var(--color-primary); }
.gruppe-item input {
accent-color: var(--color-primary);
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.gruppe-item span { flex: 1; min-width: 0; }
.item-img {
width: 38px;
height: 38px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.gruppe-item-name {
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
}
.gruppe-item-desc {
display: block;
font-size: 0.8rem;
color: var(--color-muted);
margin-top: 0.15rem;
}
.empty-hint {
color: var(--color-muted);
font-size: 0.875rem;
font-style: italic;
padding: 0.5rem 0;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 3 von 4 Aufgaben</p>
<div class="setup-section" id="sectionOwn">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listOwn">
Eigene Gruppen
</label></h2>
<ul class="gruppe-list" id="listOwn"></ul>
</div>
<div class="setup-section" id="sectionSubscribed">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listSubscribed">
Abonnierte Gruppen
</label></h2>
<ul class="gruppe-list" id="listSubscribed"></ul>
</div>
<div class="setup-section" id="sectionSystem">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listSystem">
System-Gruppen
</label></h2>
<ul class="gruppe-list" id="listSystem"></ul>
</div>
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmplayers.html'">← Zurück</button>
<button style="flex:2;" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
let savedGruppen = new Set();
async function ladeSessionOderDraft() {
if (sessionStorage.getItem('bdsm-session-setup')) return true;
try {
const res = await fetch('/bdsm/setup-draft');
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
const draft = await res.json();
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
if (!draft.setupJson) { window.location.replace('/bdsm.html'); return false; }
return true;
} catch (_) {
window.location.replace('/bdsm.html');
return false;
}
}
let warnungsAkzeptiert = false;
document.addEventListener('change', e => {
const cb = e.target;
if (cb.type !== 'checkbox') return;
if (cb.classList.contains('select-all-cb')) {
// Alle Gruppen in dieser Sektion (de-)selektieren
const list = document.getElementById(cb.dataset.list);
list.querySelectorAll('input[type="checkbox"]').forEach(itemCb => {
itemCb.checked = cb.checked;
itemCb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
});
} else {
// Einzelne Gruppe: is-checked-Klasse anpassen und Alles-Haken aktualisieren
cb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
updateSelectAll(cb.closest('.gruppe-list'));
}
warnungsAkzeptiert = false;
hideMessage();
});
function updateSelectAll(list) {
if (!list) return;
const itemCbs = [...list.querySelectorAll('input[type="checkbox"]')];
if (!itemCbs.length) return;
const section = list.closest('.setup-section');
const selectAllCb = section?.querySelector('.select-all-cb');
if (!selectAllCb) return;
const checkedCount = itemCbs.filter(cb => cb.checked).length;
selectAllCb.checked = checkedCount === itemCbs.length;
selectAllCb.indeterminate = checkedCount > 0 && checkedCount < itemCbs.length;
}
function renderList(containerId, gruppen) {
const ul = document.getElementById(containerId);
const section = ul.closest('.setup-section');
const selectAllWrap = section?.querySelector('.select-all-label');
if (!gruppen.length) {
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden';
return;
}
ul.innerHTML = gruppen.map(g => {
const checked = savedGruppen.has(g.gruppenId);
return `
<li>
<label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span>
<span class="gruppe-item-name">${g.name}</span>
${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}
</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
</label>
</li>`;
}).join('');
updateSelectAll(ul);
}
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
function validateContent(content, settings, mitspieler) {
const errors = [], warnings = [];
const aufgabenByLevel = {};
content.aufgaben.forEach(a => {
const l = a.level ?? 0;
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
});
for (const [level, count] of Object.entries(aufgabenByLevel)) {
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) Minimum 5 erforderlich`);
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben empfohlen ≥ 10`);
}
if (settings.wahrscheinlichkeitStrafe > 1) {
const strafenByLevel = {};
content.strafen.forEach(s => {
const l = s.level ?? 0;
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
});
for (const level of Object.keys(aufgabenByLevel)) {
const count = strafenByLevel[level] || 0;
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) empfohlen ≥ 2`);
}
}
if (settings.wahrscheinlichkeitSperre > 1) {
const count = content.sperren.length;
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) empfohlen ≥ 5`);
}
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
for (const g of beteiligtGeschlecht) {
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
}
return { errors, warnings };
}
function showValidation(errors, warnings, mitHinweis) {
const el = document.getElementById('message');
el.innerHTML = [
...errors.map(e => `<div>✕ ${e}</div>`),
...warnings.map(w => `<div>⚠ ${w}</div>`),
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Weiter klicken um fortzufahren.</div>'] : []),
].join('');
el.className = `message ${errors.length ? 'error' : 'warning'}`;
el.style.display = 'block';
}
function showMessage(text, type) {
const el = document.getElementById('message');
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
el.textContent = icon + text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
async function weiter() {
hideMessage();
const selected = [...document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked')]
.map(cb => cb.value);
if (selected.length === 0) {
showMessage('Bitte mindestens eine Aufgaben-Gruppe auswählen.', 'error');
warnungsAkzeptiert = false;
return;
}
const btn = document.querySelector('button[onclick="weiter()"]');
btn.disabled = true;
const gruppen = await Promise.all(
selected.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
);
btn.disabled = false;
const content = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
gruppen.filter(Boolean).forEach(g => {
content.aufgaben.push(...(g.aufgaben || []));
content.strafen.push(...(g.strafen || []));
content.sperren.push(...(g.sperren || []));
content.finisher.push(...(g.finisher || []));
});
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
const { errors, warnings } = validateContent(content, settings, setup?.mitspieler || []);
if (errors.length > 0) {
showValidation(errors, warnings, false);
warnungsAkzeptiert = false;
return;
}
if (warnings.length > 0 && !warnungsAkzeptiert) {
showValidation([], warnings, true);
warnungsAkzeptiert = true;
return;
}
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
// Draft in DB aktualisieren
fetch('/bdsm/setup-draft', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gruppenJson: JSON.stringify(selected) }),
}).catch(() => {});
window.location.href = '/bdsmtoys.html';
}
(async function init() {
const ok = await ladeSessionOderDraft();
if (!ok) return;
savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
try {
const [own, abo, system] = await Promise.all([
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
]);
renderList('listOwn', own.content || []);
renderList('listSubscribed', abo.content || []);
renderList('listSystem', system.content || []);
} catch (err) { console.error('[bdsmtasks] Fehler beim Laden:', err); }
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title>
</head>
<body>
<script>window.location.replace('/neubdsm.html');</script>
</body>
</html>

View File

@@ -1,372 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.toy-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.85rem;
border-radius: 8px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.toy-item.is-checked { border-color: var(--color-primary); }
.toy-item input {
accent-color: var(--color-primary);
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.toy-item span { flex: 1; min-width: 0; }
.item-img {
width: 38px;
height: 38px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.no-toys { color: var(--color-muted); font-size: 0.875rem; font-style: italic; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 4 von 4 Toys</p>
<div class="setup-section">
<h2>Benötigte Toys</h2>
<p style="font-size:0.85rem; color:var(--color-muted); margin-bottom:1.25rem;">
Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt.
</p>
<div id="toyList"></div>
</div>
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmtasks.html'">← Zurück</button>
<button style="flex:2;" onclick="spielStarten()">Spiel starten</button>
</div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
let savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
async function ladeSessionOderDraft() {
if (savedGruppen) return true;
try {
const res = await fetch('/bdsm/setup-draft');
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
const draft = await res.json();
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
if (draft.gruppenJson) { sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson); savedGruppen = JSON.parse(draft.gruppenJson); }
if (!savedGruppen) { window.location.replace('/bdsm.html'); return false; }
return true;
} catch (_) {
window.location.replace('/bdsm.html');
return false;
}
}
// Previously saved toy selection (when navigating back from game page)
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
const savedToyIds = savedToysRaw
? new Set(JSON.parse(savedToysRaw).map(t => t.toyId))
: null; // null = first visit → default all checked
// All content collected from selected groups
const allContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
let warnungsAkzeptiert = false;
document.addEventListener('change', e => {
const cb = e.target;
if (cb.type !== 'checkbox') return;
cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
warnungsAkzeptiert = false;
hideMessage();
});
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
function validateContent(content, settings, mitspieler) {
const errors = [], warnings = [];
const aufgabenByLevel = {};
content.aufgaben.forEach(a => {
const l = a.level ?? 0;
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
});
for (const [level, count] of Object.entries(aufgabenByLevel)) {
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) Minimum 5 erforderlich`);
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben empfohlen ≥ 10`);
}
if (settings.wahrscheinlichkeitStrafe > 1) {
const strafenByLevel = {};
content.strafen.forEach(s => {
const l = s.level ?? 0;
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
});
for (const level of Object.keys(aufgabenByLevel)) {
const count = strafenByLevel[level] || 0;
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) empfohlen ≥ 2`);
}
}
if (settings.wahrscheinlichkeitSperre > 1) {
const count = content.sperren.length;
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) empfohlen ≥ 5`);
}
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
for (const g of beteiligtGeschlecht) {
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
}
return { errors, warnings };
}
function showValidation(errors, warnings, mitHinweis) {
const el = document.getElementById('message');
el.innerHTML = [
...errors.map(e => `<div>✕ ${e}</div>`),
...warnings.map(w => `<div>⚠ ${w}</div>`),
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Spiel starten klicken um fortzufahren.</div>'] : []),
].join('');
el.className = `message ${errors.length ? 'error' : 'warning'}`;
el.style.display = 'block';
}
function showMessage(text, type) {
const el = document.getElementById('message');
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
el.textContent = icon + text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
function renderToys(toys) {
const container = document.getElementById('toyList');
if (!toys.length) {
container.innerHTML = '<p class="no-toys">Keine Toys erforderlich alle Aufgaben können gespielt werden.</p>';
return;
}
container.innerHTML = toys.map(toy => {
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
return `
<label class="toy-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
<span>
<span class="toy-item-name">${toy.name}</span>
${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}
</span>
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
</label>`;
}).join('');
}
async function spielStarten() {
const checkedToyIds = new Set(
[...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value)
);
// Collect full toy objects for the checked ones (for name display on overview)
const toyMap = new Map();
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
(item.benoetigteToys || []).forEach(t => toyMap.set(t.toyId, t));
});
const checkedToys = [...checkedToyIds].map(id => toyMap.get(id)).filter(Boolean);
sessionStorage.setItem('bdsm-session-toys', JSON.stringify(checkedToys));
function toyOk(item) {
const toys = item.benoetigteToys || [];
return toys.length === 0 || toys.every(t => checkedToyIds.has(t.toyId));
}
const gameContent = {
aufgaben: allContent.aufgaben.filter(toyOk),
strafen: allContent.strafen.filter(toyOk),
sperren: allContent.sperren.filter(toyOk),
finisher: allContent.finisher.filter(toyOk),
};
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
const btn = document.querySelector('button[onclick="spielStarten()"]');
btn.disabled = true;
// Für ACCEPTED_OWN Spieler: bereit prüfen und spielerDaten laden
const hasOwnDevice = (setup?.mitspieler || []).some(p => p.eigenesGeraet);
if (hasOwnDevice) {
const setupId = sessionStorage.getItem('bdsm-setup-id');
const einladungRes = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (einladungRes.ok) {
const einladungen = await einladungRes.json();
const nichtBereit = einladungen.filter(e => e.status === 'ACCEPTED_OWN' && !e.bereit);
if (nichtBereit.length > 0) {
showMessage('Noch nicht alle Mitspieler auf eigenem Gerät haben sich bereit erklärt.', 'error');
btn.disabled = false;
return;
}
// Spielerdaten der ACCEPTED_OWN Spieler in setup übernehmen
for (const p of setup.mitspieler) {
if (!p.eigenesGeraet || !p.einladungId) continue;
const inv = einladungen.find(e => e.einladungId === p.einladungId);
if (inv && inv.spielerDatenJson) {
const daten = JSON.parse(inv.spielerDatenJson);
p.geschlecht = daten.geschlecht;
p.spieltMit = daten.spieltMit || [];
p.rollen = daten.rollen || [];
p.werkzeuge = daten.werkzeuge || [];
p.sperrenVorFinaleAufloesen = daten.sperrenVorFinaleAufloesen !== false;
}
}
}
}
const { errors, warnings } = validateContent(gameContent, settings, setup?.mitspieler || []);
if (errors.length > 0) {
showValidation(errors, warnings, false);
warnungsAkzeptiert = false;
btn.disabled = false;
return;
}
if (warnings.length > 0 && !warnungsAkzeptiert) {
showValidation([], warnings, true);
warnungsAkzeptiert = true;
btn.disabled = false;
return;
}
sessionStorage.setItem('bdsm-session-game', JSON.stringify(gameContent));
try {
// 1. Session anlegen
const sessionRes = await fetch('/bdsm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wahrscheinlichkeitStrafe: settings.wahrscheinlichkeitStrafe,
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
setupId: sessionStorage.getItem('bdsm-setup-id'),
}),
});
if (sessionRes.status === 409) throw new Error('Du hast bereits ein laufendes BDSM-Spiel. Bitte beende es zuerst.');
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
const location = sessionRes.headers.get('Location');
const sessionId = location.split('/').pop();
// 2. Mitspieler hinzufügen (setup bereits oben geladen und mit eigenesGeraet-Daten befüllt)
for (const p of setup.mitspieler) {
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: p.name,
geschlecht: p.geschlecht,
spieltMit: p.spieltMit,
rollen: p.rollen,
verfuegbareWerkzeuge: p.werkzeuge,
userId: p.userId || null,
eigenesGeraet: p.eigenesGeraet || false,
sperrenVorFinaleAufloesen: p.sperrenVorFinaleAufloesen !== false,
}),
});
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
}
// 3. Aufgaben setzen
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContent),
});
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
sessionStorage.setItem('bdsm-session-id', sessionId);
// Draft löschen, da Spiel jetzt läuft
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
window.location.href = '/bdsmingame.html';
} catch (e) {
showMessage(e.message, 'error');
btn.disabled = false;
}
}
// Load all selected groups, collect content and unique toys
(async function init() {
const ok = await ladeSessionOderDraft();
if (!ok) return;
const gruppen = await Promise.all(
savedGruppen.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
);
gruppen.filter(Boolean).forEach(g => {
allContent.aufgaben.push(...(g.aufgaben || []));
allContent.strafen.push(...(g.strafen || []));
allContent.sperren.push(...(g.sperren || []));
allContent.finisher.push(...(g.finisher || []));
});
const toyMap = new Map();
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
(item.benoetigteToys || []).forEach(t => {
if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t);
});
});
renderToys([...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name)));
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title>
</head>
<body>
<script>window.location.replace('/neubdsm.html');</script>
</body>
</html>

View File

@@ -2,346 +2,10 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Warten XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.wait-card {
text-align: center;
padding: 3rem 1rem;
}
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
.setup-section { margin-bottom: 2rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card-field { margin-bottom: 1rem; }
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item {
display: inline-flex; align-items: flex-start; gap: 0.45rem;
background: var(--color-secondary); border: 1px solid transparent;
border-radius: 6px; padding: 0.4rem 0.7rem;
cursor: pointer; transition: border-color 0.15s; user-select: none;
}
.check-item.is-checked { border-color: var(--color-primary); }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
</style>
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title>
</head>
<body class="app">
<div class="main">
<!-- Konfigurations-Ansicht (für ACCEPTED_OWN Spieler) -->
<div class="content" id="configView" style="display:none;">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Bitte konfiguriere deine Präferenzen, bevor das Spiel startet.</p>
<div class="setup-section">
<h2>Deine Daten</h2>
<div class="card-field">
<label>Geschlecht</label>
<div class="check-group" id="geschlechtGroup"></div>
<div class="field-error" id="geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group" id="spieltMitGroup"></div>
<div class="field-error" id="spieltmit-err">Bitte mindestens eine Option wählen.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group" id="rollenGroup"></div>
<div class="field-error" id="rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col" id="werkzeugeGroup"></div>
<div class="field-error" id="werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
</div>
</div>
<div class="setup-section">
<h2>Finale</h2>
<div class="card-field">
<label class="check-item is-checked" id="sperreLabel">
<input type="checkbox" id="sperrenAufloesen" checked onchange="toggleSperreWarn()">
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
</label>
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="sperreWarn">
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Du könntest im Finale leer ausgehen.
</div>
</div>
</div>
<div class="message" id="configMessage" style="display:none;"></div>
<button onclick="bereitMachen()" style="width:100%;">Bereit</button>
<button class="secondary" style="width:100%; margin-top:0.75rem;" onclick="abbrechen()">Abbrechen</button>
</div>
<!-- Warte-Ansicht -->
<div class="content wait-card" id="waitView" style="display:none;">
<div class="wait-icon"></div>
<div class="wait-title">Warte auf Spielstart…</div>
<div class="wait-sub" id="sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
<div class="message" id="message" style="display:none;"></div>
<button class="secondary" onclick="abbrechen()">Abbrechen</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
const einladungId = params.get('id');
if (!einladungId) window.location.replace('/userhome.html');
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
function buildCheckItems(containerId, items, type) {
const container = document.getElementById(containerId);
container.innerHTML = items.map(({ value, label, desc }) => `
<label class="check-item">
<input type="${type}" name="${containerId}" value="${value}">
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function initForm() {
buildCheckItems('geschlechtGroup', GESCHLECHTER, 'radio');
buildCheckItems('spieltMitGroup', GESCHLECHTER, 'checkbox');
buildCheckItems('rollenGroup', ROLLEN, 'checkbox');
buildCheckItems('werkzeugeGroup', WERKZEUGE, 'checkbox');
document.addEventListener('change', e => {
const input = e.target;
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r =>
r.closest('.check-item')?.classList.toggle('is-checked', r.checked));
if (input.checked && input.name === 'geschlechtGroup') {
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
}
});
// Pre-fill from profile + bdsm-defaults
Promise.all([
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : null).catch(() => null),
]).then(([user, bdsmDefaults]) => {
if (user?.geschlecht) {
const radio = document.querySelector(`input[name="geschlechtGroup"][value="${user.geschlecht}"]`);
if (radio) {
radio.checked = true;
radio.closest('.check-item')?.classList.add('is-checked');
const defaults = (bdsmDefaults?.werkzeuge?.length > 0)
? bdsmDefaults.werkzeuge
: (WERKZEUGE_DEFAULTS[user.geschlecht] || []);
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
}
if (bdsmDefaults?.spieltMit?.length > 0) {
bdsmDefaults.spieltMit.forEach(val => {
const cb = document.querySelector(`input[name="spieltMitGroup"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
}
if (bdsmDefaults?.rollen?.length > 0) {
bdsmDefaults.rollen.forEach(val => {
const cb = document.querySelector(`input[name="rollenGroup"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
}
});
}
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function toggleSperreWarn() {
const cb = document.getElementById('sperrenAufloesen');
const warn = document.getElementById('sperreWarn');
const label = document.getElementById('sperreLabel');
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
if (label) label.classList.toggle('is-checked', cb.checked);
}
function setFieldError(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'block' : 'none';
}
async function bereitMachen() {
const geschlecht = getChecked('geschlechtGroup');
const spieltMit = getChecked('spieltMitGroup');
const rollen = getChecked('rollenGroup');
const werkzeuge = getChecked('werkzeugeGroup');
setFieldError('geschlecht-err', geschlecht.length === 0);
setFieldError('spieltmit-err', spieltMit.length === 0);
setFieldError('rollen-err', rollen.length === 0);
setFieldError('werkzeuge-err', werkzeuge.length === 0);
if (!geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) return;
const sperrenAufloesen = document.getElementById('sperrenAufloesen');
const spielerDatenJson = JSON.stringify({
geschlecht: geschlecht[0],
spieltMit,
rollen,
werkzeuge,
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
});
try {
const res = await fetch(`/bdsm/einladung/${einladungId}/spielerdaten`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ spielerDatenJson }),
});
if (!res.ok) throw new Error();
document.getElementById('configView').style.display = 'none';
document.getElementById('waitView').style.display = 'block';
// Sofort prüfen + Polling starten
pruefen();
pollInterval = setInterval(pruefen, 3000);
} catch (_) {
const el = document.getElementById('configMessage');
el.textContent = 'Fehler beim Speichern. Bitte erneut versuchen.';
el.className = 'message error';
el.style.display = 'block';
}
}
let pollInterval = null;
async function pruefen() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) return;
const data = await res.json();
if (data.status === 'CANCELLED') {
stopPoll();
zeigeFehler('Die Einladung wurde abgebrochen.');
return;
}
if (data.sessionId) {
try {
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
if (mRes.status === 200) {
const mData = await mRes.json();
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
sessionStorage.setItem('bdsm-guest-name', mData.name);
sessionStorage.setItem('bdsm-session-id', data.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
stopPoll();
window.location.replace('/bdsmingame.html');
}
} catch (_) {}
}
} catch (_) {}
}
function stopPoll() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function zeigeFehler(text) {
document.getElementById('waitView').style.display = 'block';
document.getElementById('configView').style.display = 'none';
document.getElementById('sub').style.display = 'none';
const el = document.getElementById('message');
el.textContent = text;
el.className = 'message error';
el.style.display = '';
}
async function abbrechen() {
stopPoll();
await fetch(`/bdsm/einladung/${einladungId}`, { method: 'DELETE' }).catch(() => {});
window.location.href = '/userhome.html';
}
// Init: check if already bereit or if config needed
async function init() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) { window.location.replace('/userhome.html'); return; }
const data = await res.json();
if (data.status === 'CANCELLED') {
document.getElementById('waitView').style.display = 'block';
zeigeFehler('Die Einladung wurde abgebrochen.');
return;
}
if (data.status === 'ACCEPTED_OWN' && !data.bereit) {
// Show config form
initForm();
document.getElementById('configView').style.display = 'block';
// Don't start polling yet user must submit form first
} else {
// Already bereit or ACCEPTED_HOST → show waiting screen + start poll
document.getElementById('waitView').style.display = 'block';
pruefen();
pollInterval = setInterval(pruefen, 3000);
}
} catch (_) {
window.location.replace('/userhome.html');
}
}
init();
</script>
<body>
<script>window.location.replace('/neubdsm.html');</script>
</body>
</html>

View File

@@ -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.';

View File

@@ -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 {

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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.
</p>
<a href="/bdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
<a href="/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">