From 528ea89bc4f5cc4843111bcbd63b73f3a159136f Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 24 Mar 2026 22:51:27 +0100 Subject: [PATCH] Anbindung an TTLock umgesetzt --- .claude/settings.local.json | 5 +- .metadata/.lock_info | 4 +- .metadata/.log | 372 ++ .../.safetable/org.eclipse.core.resources | Bin 1307 -> 1307 bytes .../org.eclipse.e4.workbench/workbench.xmi | 3987 +++++++++-------- .../variablesAndContainers.dat | Bin 39753 -> 40109 bytes .../.plugins/org.eclipse.m2e.logback/0.log | 1 + .metadata/version.ini | 2 +- bilder/toys/doppelpnsknebel.png | Bin 0 -> 13930 bytes bilder/toys/raw/41-54Dp0rLL._AC_SY450_.jpg | Bin 0 -> 14860 bytes xxxthegame/build.gradle.kts | 1 + .../de/oaa/xxx/admin/AdminController.java | 151 +- .../de/oaa/xxx/config/SecurityConfig.java | 4 + .../chastity/cardlock/CardLockController.java | 57 +- .../chastity/cardlock/CardLockRepository.java | 5 + .../chastity/cardlock/CardLockService.java | 46 +- .../cardlock/CardLockServiceFactory.java | 17 +- .../games/chastity/common/BaseLockEntity.java | 4 + .../chastity/common/BaseLockRepository.java | 4 +- .../chastity/common/BaseLockService.java | 28 +- .../KeyholderNotificationEntity.java | 1 + .../chastity/lockcontroll/LockControl.java | 2 + .../lockcontroll/LockControlFactory.java | 42 + .../chastity/lockcontroll/TTLockControl.java | 153 +- .../lockcontroll/TrustLockControl.java | 5 + .../lockcontroll/UnlockcodeLockControl.java | 5 + .../chastity/timelock/TimeLockController.java | 31 +- .../chastity/timelock/TimeLockRepository.java | 5 + .../chastity/timelock/TimeLockService.java | 26 +- .../timelock/TimeLockServiceFactory.java | 10 +- .../games/chastity/ttlock/TTAuthService.java | 38 + .../games/chastity/ttlock/TTLockCallback.java | 225 + .../chastity/ttlock/TTLockConfigEntity.java | 26 + .../ttlock/TTLockConfigRepository.java | 5 + .../games/chastity/ttlock/TTLockService.java | 169 + .../xxx/games/chastity/ttlock/TTLockTest.java | 56 + .../ttlock/TTLockUserConfigEntity.java | 32 + .../ttlock/TTLockUserConfigRepository.java | 11 + .../oaa/xxx/games/chastity/ttlock/unlocktypes | 121 + .../chastity/unlock/TempOpeningReason.java | 2 +- .../UserSubscriptionRepository.java | 3 + .../java/de/oaa/xxx/user/UserController.java | 155 +- .../src/main/resources/application.properties | 3 + .../src/main/resources/static/admin.html | 289 +- .../src/main/resources/static/aufgaben.html | 32 +- .../main/resources/static/einstellungen.html | 194 + .../src/main/resources/static/keyholder.html | 28 +- .../src/main/resources/static/neulock.html | 185 +- 48 files changed, 4577 insertions(+), 1965 deletions(-) create mode 100644 bilder/toys/doppelpnsknebel.png create mode 100644 bilder/toys/raw/41-54Dp0rLL._AC_SY450_.jpg create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControlFactory.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTAuthService.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockCallback.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigEntity.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigRepository.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockService.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockTest.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigEntity.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigRepository.java create mode 100644 xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/unlocktypes diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8c0faa3..3f08b9f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,7 +36,10 @@ "Bash(__NEW_LINE_20ef1f88f3630ae7__ done:*)", "Bash(__NEW_LINE_1bd3b9012681f121__ grep:*)", "Bash(__NEW_LINE_1bd3b9012681f121__ done:*)", - "Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)" + "Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)", + "Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)", + "Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)", + "Bash(1:*)" ] } } diff --git a/.metadata/.lock_info b/.metadata/.lock_info index 463fed8..0297dc6 100644 --- a/.metadata/.lock_info +++ b/.metadata/.lock_info @@ -1,5 +1,5 @@ -#Tue Mar 24 06:41:43 CET 2026 +#Tue Mar 24 11:25:59 CET 2026 display=\:0 host=mario-mint -process-id=4231 +process-id=2972 user=mario diff --git a/.metadata/.log b/.metadata/.log index 7390395..19646a1 100644 --- a/.metadata/.log +++ b/.metadata/.log @@ -311,3 +311,375 @@ Binding(CTRL+SHIFT+T, ,,true),null), org.eclipse.ui.defaultAcceleratorConfiguration, org.eclipse.ui.contexts.window,,,system) + +!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-24 08:03:25.447 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS +!SESSION 2026-03-24 11:25:55.699 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.6 +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 -clean -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-24 11:25:57.216 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-24 11:26:00.128 +!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-24 11:26:00.273 +!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-24 11:26:00.273 +!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-24 11:26:00.403 +!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-24 11:26:00.403 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.jface 2 0 2026-03-24 11:42:02.158 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-24 11:42:02.158 +!MESSAGE A conflict occurred for CTRL+SHIFT+T: +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type, + Open a type in a Java editor, + Category(org.eclipse.ui.category.navigate,Navigate,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) +Binding(CTRL+SHIFT+T, + ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace, + , + Category(org.eclipse.lsp4e.category,Language Servers,null,true), + WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.ui.contexts.window,,,system) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 11:49:55.052 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.jface 2 0 2026-03-24 11:54:46.436 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-24 11:54:46.436 +!MESSAGE A conflict occurred for CTRL+R: +Binding(CTRL+R, + ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line, + Resume and break when execution reaches the current line, + Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true), + WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.debug.ui.debugging,,,system) +Binding(CTRL+R, + ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart, + Restart Spring Boot Application, + Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true), + WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.debug.ui.console,,,system) + +!ENTRY org.eclipse.jdt.debug.ui 4 150 2026-03-24 11:55:55.048 +!MESSAGE Internal Error +!STACK 1 +org.eclipse.debug.core.DebugException: Invalid stack frame + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1378) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) +Caused by: java.lang.IllegalStateException + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381) + ... 12 more +!SUBENTRY 1 org.eclipse.jdt.debug 4 100 2026-03-24 11:55:55.048 +!MESSAGE Invalid stack frame +!STACK 0 +java.lang.IllegalStateException + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.jdt.debug.ui 4 150 2026-03-24 11:59:03.920 +!MESSAGE Internal Error +!STACK 1 +org.eclipse.debug.core.DebugException: Invalid stack frame + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1378) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) +Caused by: java.lang.IllegalStateException + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381) + ... 12 more +!SUBENTRY 1 org.eclipse.jdt.debug 4 100 2026-03-24 11:59:03.920 +!MESSAGE Invalid stack frame +!STACK 0 +java.lang.IllegalStateException + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397) + at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604) + at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 12:19:36.658 +!MESSAGE Timeout waiting for data to generate LS hover +!STACK 0 +java.util.concurrent.TimeoutException + at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1960) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2095) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.ui 4 0 2026-03-24 12:42:09.972 +!MESSAGE Unhandled event loop exception +!STACK 0 +org.eclipse.swt.SWTException: Widget is disposed + at org.eclipse.swt.SWT.error(SWT.java:4950) + at org.eclipse.swt.SWT.error(SWT.java:4865) + at org.eclipse.swt.SWT.error(SWT.java:4836) + at org.eclipse.swt.widgets.Widget.error(Widget.java:598) + at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:513) + at org.eclipse.swt.widgets.Control.redraw(Control.java:4613) + at org.eclipse.swt.widgets.Link.gtk3_event_after(Link.java:453) + at org.eclipse.swt.widgets.Widget.windowProc(Widget.java:2627) + at org.eclipse.swt.widgets.Control.windowProc(Control.java:6833) + at org.eclipse.swt.widgets.Display.windowProc(Display.java:6152) + at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_do_event(Native Method) + at org.eclipse.swt.widgets.Display.eventProc(Display.java:1624) + at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_iteration_do(Native Method) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4494) + 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) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 13:07:28.883 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 13:16:03.467 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 16:35:09.823 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 16:39:30.159 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.debug.core 4 125 2026-03-24 17:38:12.471 +!MESSAGE Error logged from Debug Core: +!STACK 0 +java.io.IOException: Stream closed + at java.base/java.io.BufferedInputStream.getBufIfOpen(BufferedInputStream.java:188) + at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:343) + at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420) + at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:405) + at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95) + at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235) + at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 17:44:12.426 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:10:52.515 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:12:21.030 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:52:19.848 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:52:33.597 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 20:00:50.619 +!MESSAGE Javadoc unavailable. Failed to obtain it. +!STACK 0 +java.lang.InterruptedException + at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386) + at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) + at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165) + at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131) + at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89) + at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155) + +!ENTRY org.eclipse.ui 4 0 2026-03-24 21:34:43.852 +!MESSAGE Unhandled event loop exception +!STACK 0 +java.lang.NullPointerException: Cannot invoke "org.eclipse.e4.ui.model.application.ui.MElementContainer.getChildren()" because "root" is null + at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.findFirstPartStackToBeRendered(CleanupAddon.java:397) + at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.transferPrimaryDataStackIfRemoved(CleanupAddon.java:390) + at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.lambda$1(CleanupAddon.java:356) + at org.eclipse.swt.widgets.RunnableLock.run(RunnableLock.java:40) + at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:132) + at org.eclipse.swt.widgets.Display.runAsyncMessages(Display.java:5035) + at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4500) + 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) diff --git a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources index 3a0eb33075004424d449e4d20e54590b716aead7..32e55a0dd0b24d8899af0b9a3a78c1ba869e6dfd 100644 GIT binary patch delta 166 zcmbQuHJfXK{N&ZlJdzex2F6xKrV7rkAqqwYMrK_4dL>1vskRoA7cxmq=4WBs{Ek_c tadJJQ7KgE^g@LK5g~{ZNj0QyNRbldHMAyeHWoT$+Y++?=fY(x8MgZj_DnI}L delta 177 zcmbQuHJfXKysB@0o`P>;k%Eyi5Lp3{iKT+GYlwo8fsq-PzFtXDYO1aI - - + + activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration - + @@ -11,9 +11,9 @@ topLevel shellMaximized - - - + + + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -83,119 +83,122 @@ persp.editorOnboardingCommand:Show Key Assist$$$Shift+Ctrl+L persp.editorOnboardingCommand:New$$$Ctrl+N persp.editorOnboardingCommand:Open Type$$$Shift+Ctrl+T - - - - + + + + org.eclipse.e4.primaryNavigationStack - + active + noFocus + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - + Minimized - + View categoryTag:Spring - - + + View categoryTag:Git - - - - + + + + org.eclipse.e4.secondaryNavigationStack - + Minimized + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Java - + View categoryTag:Ant - + org.eclipse.e4.secondaryDataStack Oomph Gradle - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Gradle - + View categoryTag:Gradle - + View categoryTag:Oomph NoRestore @@ -204,2565 +207,2827 @@ + + + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation + persp.actionSet:org.eclipse.ui.cheatsheets.actionSet + persp.actionSet:org.eclipse.search.searchActionSet + persp.actionSet:org.eclipse.text.quicksearch.actionSet + persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo + persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet + persp.actionSet:org.eclipse.ui.actionSet.keyBindings + persp.actionSet:org.eclipse.ui.actionSet.openFiles + persp.actionSet:org.springsource.ide.eclipse.commons.launch.actionSet + persp.viewSC:org.eclipse.ui.views.ProgressView + persp.viewSC:org.eclipse.ui.texteditor.TemplatesView + persp.actionSet:org.eclipse.debug.ui.launchActionSet + persp.actionSet:org.eclipse.debug.ui.debugActionSet + persp.actionSet:org.eclipse.ui.NavigateActionSet + persp.viewSC:org.eclipse.debug.ui.DebugView + persp.viewSC:org.eclipse.debug.ui.VariableView + persp.viewSC:org.eclipse.debug.ui.BreakpointView + persp.viewSC:org.eclipse.debug.ui.ExpressionView + persp.viewSC:org.eclipse.ui.views.ContentOutline + persp.viewSC:org.eclipse.ui.console.ConsoleView + persp.viewSC:org.eclipse.ui.views.ProblemView + persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer + persp.viewSC:org.eclipse.pde.runtime.LogView + persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.debug.ui/icons/full/onboarding_debug_persp.svg + persp.editorOnboardingText:Go hunt your bugs here. + persp.actionSet:org.eclipse.debug.ui.breakpointActionSet + persp.showIn:org.eclipse.ui.navigator.ProjectExplorer + persp.editorOnboardingCommand:Find Actions$$$Ctrl+3 + persp.editorOnboardingCommand:Step Into$$$F5 + persp.editorOnboardingCommand:Step Over$$$F6 + persp.editorOnboardingCommand:Step Return$$$F7 + persp.editorOnboardingCommand:Resume$$$F8 + persp.perspSC:org.eclipse.jdt.ui.JavaPerspective + persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective + persp.actionSet:org.eclipse.jdt.ui.JavaActionSet + persp.showIn:org.eclipse.jdt.ui.PackageExplorer + persp.viewSC:org.eclipse.terminal.view.ui.TerminalsView + persp.showIn:org.eclipse.terminal.view.ui.TerminalsView + persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet + persp.viewSC:org.eclipse.jdt.debug.ui.DisplayView + persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet + persp.showIn:org.eclipse.eclemma.ui.CoverageView + persp.showIn:org.eclipse.egit.ui.RepositoriesView + persp.viewSC:org.eclipse.jdt.junit.ResultView + persp.viewSC:org.eclipse.ant.ui.views.AntView + + + org.eclipse.e4.primaryNavigationStack + + View + categoryTag:Debug + + + View + categoryTag:General + + + View + categoryTag:Java + + + View + categoryTag:Java + + + View + categoryTag:Java + + + + + + + org.eclipse.e4.secondaryNavigationStack + Minimized + + View + categoryTag:Debug + + + View + categoryTag:Debug + + + View + categoryTag:Debug + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Ant + + + + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Debug + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Terminal + + + View + categoryTag:Debug + + + View + categoryTag:General + + + + + - - + + View categoryTag:Help - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:Help - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Help - - + + EditorStack - org.eclipse.e4.primaryDataStack - active - - + + Editor removeOnHide org.eclipse.jdt.ui.CompilationUnitEditor - - + + Editor removeOnHide org.eclipse.jdt.ui.CompilationUnitEditor - - - Editor - removeOnHide - SpringBootPropertyEditor - - - + + Editor removeOnHide org.eclipse.jdt.ui.CompilationUnitEditor - - + + Editor removeOnHide - org.eclipse.ui.genericeditor.GenericEditor - active + org.eclipse.jdt.ui.CompilationUnitEditor + + + + Editor + removeOnHide + org.eclipse.jdt.ui.CompilationUnitEditor - + - + View categoryTag:Java - + active + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:General - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:Java - + + View categoryTag:General + + ViewMenu + menuContribution:menu + + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + + View categoryTag:General + + ViewMenu + menuContribution:menu + + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Terminal - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Java - + View categoryTag:Spring - + ViewMenu menuContribution:menu - + - + View categoryTag:Ant - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Oomph NoRestore - + ViewMenu menuContribution:menu - + - - + + + + + View + categoryTag:Debug + busy + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Debug + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + + View + categoryTag:Debug + + ViewMenu + menuContribution:menu + + + + + toolbarSeparator - + - + Draggable - + - + toolbarSeparator - + - + Draggable - - + + - + toolbarSeparator - + - + Draggable - + Draggable - + Draggable - + Draggable - + toolbarSeparator - + - + Draggable - + - + Draggable - - Draggable - - - Draggable - - + toolbarSeparator - + - + toolbarSeparator - + - + Draggable - + stretch SHOW_RESTORE_MENU - + Draggable HIDEABLE SHOW_RESTORE_MENU - - + + stretch - + Draggable - + Draggable - - + + TrimStack Draggable - + + TrimStack + Draggable + + + TrimStack + Draggable + + + TrimStack + Draggable + + + + + TrimStack + Draggable + + + + + TrimStack + Draggable + + + TrimStack + Draggable + + TrimStack Draggable - - - - - - - - - - - - - - - + + + + + + + + + + + + + + platform:gtk - - - - + + + + platform:gtk - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - + + + - - - - - + + + + + - - + + - - - + + + - - - + + + - - - - - - - - + + + + + + + + platform:gtk - - - - - + + + + + - - + + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - + + - - - - - - + + + + + + - - - - - - + + + + + + - - + + - - - - - - - - + + + + + + + + - - - + + + - - - - + + + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Editor removeOnHide - + View categoryTag:Ant - + View categoryTag:Gradle - + View categoryTag:Gradle - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git NoRestore - + View categoryTag:Git - + View categoryTag:Help - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Oomph - + View categoryTag:Oomph NoRestore - + View categoryTag:Plug-in Development - + View categoryTag:General - + View categoryTag:Version Control (Team) - + View categoryTag:Version Control (Team) - + View categoryTag:Terminal - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Spring - + View categoryTag:Spring - + View categoryTag:Spring - - + + glue move_after:PerspectiveSpacer SHOW_RESTORE_MENU - + move_after:Spacer Glue HIDEABLE SHOW_RESTORE_MENU - + glue move_after:SearchField SHOW_RESTORE_MENU - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat index c95e62e11db7511dcf810f9fa3facd8e18f35937..17dab3e5aee7a95ff51b4c10d90088aaf27314a4 100644 GIT binary patch delta 4020 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 diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/0.log b/.metadata/.plugins/org.eclipse.m2e.logback/0.log index 41ee59b..80de81c 100644 --- a/.metadata/.plugins/org.eclipse.m2e.logback/0.log +++ b/.metadata/.plugins/org.eclipse.m2e.logback/0.log @@ -6,3 +6,4 @@ 2026-03-23 17:38:51,039 [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-23 21:09:44,347 [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-24 06:41:47,661 [Worker-2: 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-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. diff --git a/.metadata/version.ini b/.metadata/version.ini index 244c875..7c59b45 100644 --- a/.metadata/version.ini +++ b/.metadata/version.ini @@ -1,3 +1,3 @@ -#Tue Mar 24 06:41:43 CET 2026 +#Tue Mar 24 11:25:59 CET 2026 org.eclipse.core.runtime=2 org.eclipse.platform=4.39.0.v20260226-0420 diff --git a/bilder/toys/doppelpnsknebel.png b/bilder/toys/doppelpnsknebel.png new file mode 100644 index 0000000000000000000000000000000000000000..9a06a5db344f02fd38fe465a6bc38b8b9fde510e GIT binary patch literal 13930 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 gjc3ha{RzEIdOuUuic}kTzm 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 F{{vMZFWUeB literal 0 HcmV?d00001 diff --git a/xxxthegame/build.gradle.kts b/xxxthegame/build.gradle.kts index df07ce0..c40335c 100644 --- a/xxxthegame/build.gradle.kts +++ b/xxxthegame/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-mail") + implementation("commons-codec:commons-codec:1.16.0") runtimeOnly("com.mysql:mysql-connector-j") implementation("io.jsonwebtoken:jjwt-api:0.12.6") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6") diff --git a/xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java b/xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java index 0f59cea..b68ecb8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java @@ -1,5 +1,25 @@ package de.oaa.xxx.admin; +import java.security.Principal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import de.oaa.xxx.aufgaben.AufgabenGruppe; import de.oaa.xxx.aufgaben.Toy; import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity; @@ -11,21 +31,16 @@ 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.games.chastity.ttlock.TTLockConfigEntity; +import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; import de.oaa.xxx.meldung.MeldungEntity; import de.oaa.xxx.meldung.MeldungRepository; import de.oaa.xxx.meldung.MeldungStatus; +import de.oaa.xxx.subscription.SubscriptionType; +import de.oaa.xxx.subscription.UserSubscriptionEntity; +import de.oaa.xxx.subscription.UserSubscriptionRepository; import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserRepository; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.security.Principal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; @RestController @RequestMapping("/admin") @@ -42,6 +57,8 @@ public class AdminController { private final FinisherRepository finisherRepository; private final GruppenAboRepository gruppenAboRepository; private final ToyRepository toyRepository; + private final TTLockConfigRepository ttLockConfigRepository; + private final UserSubscriptionRepository userSubscriptionRepository; public AdminController(AdminRepository adminRepository, UserRepository userRepository, MeldungRepository meldungRepository, @@ -51,7 +68,9 @@ public class AdminController { SperreRepository sperreRepository, FinisherRepository finisherRepository, GruppenAboRepository gruppenAboRepository, - ToyRepository toyRepository) { + ToyRepository toyRepository, + TTLockConfigRepository ttLockConfigRepository, + UserSubscriptionRepository userSubscriptionRepository) { this.adminRepository = adminRepository; this.userRepository = userRepository; this.meldungRepository = meldungRepository; @@ -62,12 +81,18 @@ public class AdminController { this.finisherRepository = finisherRepository; this.gruppenAboRepository = gruppenAboRepository; this.toyRepository = toyRepository; + this.ttLockConfigRepository = ttLockConfigRepository; + this.userSubscriptionRepository = userSubscriptionRepository; } // ── DTOs ───────────────────────────────────────────────────────────────── record AdminDto(UUID adminId, UUID userId, String userName, AdminRolle rolle, LocalDateTime createdAt) {} + record TtlockConfigDto(String clientId, String clientSecret, String baseUrl) {} + + record TtlockConfigRequest(String clientId, String clientSecret, String baseUrl) {} + record MeldungDto(UUID meldungId, UUID melderId, String melderName, de.oaa.xxx.meldung.MeldungZielTyp zielTyp, UUID zielId, String grund, LocalDateTime gemeldetAt, @@ -79,6 +104,11 @@ public class AdminController { record UserSearchDto(UUID userId, String name) {} + record GiftSubscriptionRequest(UUID userId) {} + + record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType, + LocalDate subscribedAt, LocalDate validUntil) {} + // ── Hilfsmethoden ──────────────────────────────────────────────────────── private AdminEntity requireAdmin(Principal principal) { @@ -274,6 +304,18 @@ public class AdminController { .toList()); } + @GetMapping("/users/search/all") + public ResponseEntity> searchAllUsers( + @RequestParam String q, Principal principal) { + requireSuperAdmin(principal); + if (q == null || q.isBlank()) return ResponseEntity.ok(List.of()); + List users = userRepository.findByNameContainingIgnoreCase(q.trim()); + return ResponseEntity.ok(users.stream() + .limit(20) + .map(u -> new UserSearchDto(u.getUserId(), u.getName())) + .toList()); + } + // ── Admin-Verwaltung (nur SUPERADMIN) ──────────────────────────────────── @GetMapping("/admins") @@ -309,4 +351,91 @@ public class AdminController { adminRepository.delete(entity); return ResponseEntity.noContent().build(); } + + // ── Abonnement verschenken (nur SUPERADMIN) ────────────────────────────── + + @GetMapping("/subscriptions") + public ResponseEntity> getAllSubscriptions(Principal principal) { + requireSuperAdmin(principal); + var activeSubscriptions = userSubscriptionRepository + .findByValidUntilGreaterThanEqualOrderByValidUntilDesc(LocalDate.now()); + return ResponseEntity.ok(activeSubscriptions.stream().map(sub -> { + String name = userRepository.findById(sub.getUserId()).map(UserEntity::getName).orElse("?"); + return new SubscriptionStatusDto(sub.getUserId(), name, + sub.getSubscriptionType().name(), sub.getSubscribedAt(), sub.getValidUntil()); + }).toList()); + } + + @GetMapping("/subscriptions/user/{userId}") + public ResponseEntity getSubscriptionStatus( + @PathVariable UUID userId, Principal principal) { + requireSuperAdmin(principal); + UserEntity user = userRepository.findById(userId).orElse(null); + if (user == null) return ResponseEntity.notFound().build(); + var sub = userSubscriptionRepository + .findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(userId, LocalDate.now()) + .orElse(null); + return ResponseEntity.ok(new SubscriptionStatusDto( + userId, user.getName(), + sub != null ? sub.getSubscriptionType().name() : "STANDARD", + sub != null ? sub.getSubscribedAt() : null, + sub != null ? sub.getValidUntil() : null + )); + } + + @PostMapping("/subscriptions/gift") + public ResponseEntity giftSubscription( + @RequestBody GiftSubscriptionRequest request, Principal principal) { + requireSuperAdmin(principal); + UserEntity user = userRepository.findById(request.userId()).orElse(null); + if (user == null) return ResponseEntity.notFound().build(); + + LocalDate today = LocalDate.now(); + var existing = userSubscriptionRepository + .findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(request.userId(), today) + .orElse(null); + + UserSubscriptionEntity sub = new UserSubscriptionEntity(); + sub.setUserId(request.userId()); + sub.setSubscriptionType(SubscriptionType.PREMIUM); + sub.setSubscribedAt(today); + // Hat der User bereits ein aktives Abo: Laufzeit um 1 Monat verlängern + sub.setValidUntil(existing != null + ? existing.getValidUntil().plusMonths(1) + : today.plusMonths(1)); + sub.setCancellableFrom(null); // Geschenk, kein Vertrag + userSubscriptionRepository.save(sub); + + return ResponseEntity.ok(new SubscriptionStatusDto( + request.userId(), user.getName(), + sub.getSubscriptionType().name(), + sub.getSubscribedAt(), sub.getValidUntil() + )); + } + + // ── TTLock-Konfiguration (nur SUPERADMIN) ───────────────────────────────── + + @GetMapping("/ttlock") + public ResponseEntity getTtlockConfig(Principal principal) { + requireSuperAdmin(principal); + TTLockConfigEntity cfg = ttLockConfigRepository.findById(1L) + .orElse(new TTLockConfigEntity()); + return ResponseEntity.ok(new TtlockConfigDto( + cfg.getClientId(), + cfg.getClientSecret(), + cfg.getBaseUrl() + )); + } + + @PutMapping("/ttlock") + public ResponseEntity saveTtlockConfig(@RequestBody TtlockConfigRequest body, Principal principal) { + requireSuperAdmin(principal); + TTLockConfigEntity cfg = ttLockConfigRepository.findById(1L) + .orElseGet(TTLockConfigEntity::new); + cfg.setClientId(body.clientId()); + cfg.setClientSecret(body.clientSecret()); + cfg.setBaseUrl(body.baseUrl()); + ttLockConfigRepository.save(cfg); + return ResponseEntity.noContent().build(); + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java index 6a2c5d1..a3508ac 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java @@ -35,6 +35,7 @@ public class SecurityConfig { .dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR).permitAll() .requestMatchers("/").permitAll() .requestMatchers("/error").permitAll() + .requestMatchers("/api").permitAll() .requestMatchers("/userhome.html").authenticated() .requestMatchers("/toys.html").authenticated() .requestMatchers("/aufgaben.html").authenticated() @@ -86,6 +87,7 @@ public class SecurityConfig { .requestMatchers("/*.svg").permitAll() .requestMatchers("/*.webp").permitAll() .requestMatchers(HttpMethod.GET, "/login").permitAll() + .requestMatchers(HttpMethod.GET, "/ttlock").permitAll() .requestMatchers(HttpMethod.POST, "/login").permitAll() .requestMatchers(HttpMethod.GET, "/login/publickey").permitAll() .requestMatchers(HttpMethod.GET, "/login/logout").permitAll() @@ -99,6 +101,8 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/email-change/**").permitAll() .requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll() .requestMatchers(HttpMethod.POST, "/filler").permitAll() + .requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll() + .requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java index d8f8995..82594cf 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java @@ -46,6 +46,8 @@ import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory; +import de.oaa.xxx.games.chastity.lockcontroll.LockControllType; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; @@ -56,6 +58,7 @@ import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryRepository; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; import de.oaa.xxx.social.SystemMessageService; +import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.user.UserRepository; @RestController @@ -76,6 +79,7 @@ public class CardLockController { private final UnlockCodeHistoryService unlockCodeHistoryService; private final SystemMessageService systemMessageService; private final CardLockServiceFactory cardLockServiceFactory; + private final SubscriptionLimitService subscriptionLimitService; @Value("${app.base-url:http://localhost:8080}") private String baseUrl; @@ -90,10 +94,12 @@ public class CardLockController { AssignedTaskRepository assignedTaskRepository, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, CommunityTaskVoteRepository communityTaskVoteRepository, - UnlockCodeHistoryRepository unlockCodeHistoryRepository, + UnlockCodeHistoryRepository unlockCodeHistoryRepository, UnlockCodeHistoryService unlockCodeHistoryService, - SystemMessageService systemMessageService, - CardLockServiceFactory cardLockServiceFactory) { + SystemMessageService systemMessageService, + CardLockServiceFactory cardLockServiceFactory, + LockControlFactory lockControlFactory, + SubscriptionLimitService subscriptionLimitService) { this.cardlockRepository = cardlockRepository; this.userRepository = userRepository; this.invitationRepository = invitationRepository; @@ -108,13 +114,14 @@ public class CardLockController { this.unlockCodeHistoryService = unlockCodeHistoryService; this.systemMessageService = systemMessageService; this.cardLockServiceFactory = cardLockServiceFactory; + this.subscriptionLimitService = subscriptionLimitService; } record CreateCardLockRequest(String name, UUID keyholder, UUID lockeeUserId, boolean lockeeDetailsVisible, List initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards, LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites, List tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines, - TaskMode taskMode) { + TaskMode taskMode, LockControllType controllType) { } private static final SecureRandom RNG = new SecureRandom(); @@ -190,8 +197,12 @@ public class CardLockController { if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE; + if (controllType == LockControllType.TTLOCK && !subscriptionLimitService.hasActivePaidSubscription(myId)) { + return ResponseEntity.status(403).body(Map.of("error", "subscription_required")); + } + int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5; - String unlockCode = generateUnlockCode(codeLines); CardLockEntity lock = new CardLockEntity(); lock.setName(req.name()); @@ -209,7 +220,7 @@ public class CardLockController { lock.setTestLock(req.testLock()); lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM); lock.setUnlockCodeLength(codeLines); - lock.setUnlockCode(unlockCode); + lock.setControllType(controllType); LocalDateTime now = LocalDateTime.now(); lock.setStartTime(now); @@ -219,8 +230,17 @@ public class CardLockController { if (req.hygineOpeningEveryMinites() != null) { lock.setLastHygineOpening(now); } + cardlockRepository.save(lock); // erst speichern, damit Lock-ID vorhanden ist - cardlockRepository.save(lock); + // Initialen Unlock-Code / TTLock-PIN via LockControl setzen + CardLockService initService = cardLockServiceFactory.create(lock); + if (initService.getLockControl() != null) { + initService.getLockControl().lock(); + } else { + // Fallback: direkte Code-Generierung (UNLOCK_CODE ohne Factory) + lock.setUnlockCode(generateUnlockCode(codeLines)); + cardlockRepository.save(lock); + } boolean keyholderPending = false; if (req.keyholder() != null) { @@ -246,7 +266,7 @@ public class CardLockController { } } - return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "unlockCode", unlockCode, + return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "unlockCode", lock.getUnlockCode(), "keyholderPending", keyholderPending)); } @@ -350,6 +370,24 @@ public class CardLockController { return ResponseEntity.noContent().build(); } + @PostMapping("/cardlock/{lockId}/relock") + @Transactional + public ResponseEntity relock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = cardlockRepository.findById(lockId); + if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); + if (l.getControllType() != LockControllType.TTLOCK) return ResponseEntity.status(409).build(); + + var lc = cardLockServiceFactory.create(l).getLockControl(); + if (lc != null) lc.lock(); + return ResponseEntity.noContent().build(); + } + @PostMapping("/cardlock/{lockId}/green/keep") @Transactional public ResponseEntity greenKeep(@PathVariable UUID lockId, Principal principal) { @@ -916,7 +954,8 @@ public class CardLockController { var notification = keyholderNotificationRepository.findByLockId(lockId).stream() .sorted((a, b) -> b.getViolationTime().compareTo(a.getViolationTime())).limit(5) - .map(v -> Map.of("time", v.getViolationTime().toString(), "overtimeMinutes", v.getOvertimeMinutes())) + .map(v -> Map.of("time", v.getViolationTime().toString(), "overtimeMinutes", v.getOvertimeMinutes(), + "openingReason", v.getOpeningReason() != null ? v.getOpeningReason().name() : "HYGIENE")) .toList(); Map result = new HashMap<>(); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockRepository.java index ce1b2ad..e379070 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockRepository.java @@ -3,7 +3,12 @@ package de.oaa.xxx.games.chastity.cardlock; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface CardLockRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM CardLockEntity c WHERE c.lockId = :lockId") + void deleteByLockId(UUID lockId); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java index 01d73eb..1596549 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java @@ -18,6 +18,8 @@ import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; @@ -26,13 +28,13 @@ import de.oaa.xxx.games.history.GameType; import de.oaa.xxx.social.SystemMessageService; import de.oaa.xxx.user.UserRepository; -public class CardLockService extends BaseLockService { +public class CardLockService extends BaseLockService implements LockControlCallback { private static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class); private final CardLockEntity lock; private final CardLockRepository cardLockRepository; private String pendingTaskMode; - + public CardLockService( CardLockEntity lock, CommunityVerificationVoteRepository communityVerificationVoteRepository, @@ -45,12 +47,37 @@ public class CardLockService extends BaseLockService { UnlockCodeHistoryService unlockCodeHistoryService, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, CommunityTaskVoteRepository communityTaskVoteRepository, - CardLockRepository cardLockRepository) { + CardLockRepository cardLockRepository, + LockControlFactory lockControlFactory) { super(communityVerificationVoteRepository, communityVerificationRepository, keyholderVerificationRepository, gameHistoryRepository, userRepository, keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService, keyholderTaskChoiceRepository, communityTaskVoteRepository); this.lock = lock; this.cardLockRepository = cardLockRepository; + // lockControl aus Entity-Typ wiederherstellen (für bereits laufende Locks) + if (lock.getControllType() != null) { + this.lockControl = lockControlFactory.create(lock.getControllType(), this, lock.getLockee()); + } + } + + // ── LockControl Setup ───────────────────────────────────────────────────── + + /** Wird von CardLockServiceFactory gesetzt (package-private). */ + void initLockControl(de.oaa.xxx.games.chastity.lockcontroll.LockControl lc) { + this.lockControl = lc; + } + + // ── LockControlCallback ─────────────────────────────────────────────────── + + @Override + public void setUnlockCode(String code) { + lock.setUnlockCode(code); + cardLockRepository.save(lock); + } + + @Override + public int getUnlockcodeLenght() { + return lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5; } // ── Abstract method implementations ────────────────────────────────────── @@ -206,6 +233,11 @@ public class CardLockService extends BaseLockService { // ── Hygiene opening ─────────────────────────────────────────────────────── + @Override + protected void afterHygieneClosing() { + if (lockControl != null) lockControl.lock(); + } + public void startHygieneOpening() { startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes()); } @@ -232,7 +264,13 @@ public class CardLockService extends BaseLockService { lock.setTempOpeningTime(null); lock.setTempOpeningReason(null); - var code = CodeCreator.createNumeric(lock.getUnlockCodeLength()); + if (lockControl != null + && lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) { + lockControl.lock(); + cardLockRepository.save(lock); + return lock.getUnlockCode() != null ? lock.getUnlockCode() : ""; + } + var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5); lock.setUnlockCode(code); cardLockRepository.save(lock); return code; diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java index 6c15b66..3881ca2 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java @@ -7,6 +7,7 @@ import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; import de.oaa.xxx.social.SystemMessageService; import de.oaa.xxx.user.UserRepository; @@ -33,6 +34,7 @@ public class CardLockServiceFactory { private final SystemMessageService systemMessageService; private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; private final CommunityTaskVoteRepository communityTaskVoteRepository; + private final LockControlFactory lockControlFactory; public CardLockServiceFactory( CommunityVerificationRepository communityVerificationRepository, @@ -45,7 +47,8 @@ public class CardLockServiceFactory { UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, - CommunityTaskVoteRepository communityTaskVoteRepository) { + CommunityTaskVoteRepository communityTaskVoteRepository, + LockControlFactory lockControlFactory) { this.cardLockRepository = cardLockRepository; this.communityVerificationRepository = communityVerificationRepository; this.communityVerificationVoteRepository = communityVerificationVoteRepository; @@ -57,15 +60,19 @@ public class CardLockServiceFactory { this.systemMessageService = systemMessageService; this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.communityTaskVoteRepository = communityTaskVoteRepository; + this.lockControlFactory = lockControlFactory; } /** * Erstellt eine neue CardLockService-Instanz für das gegebene Lock. + * Setzt den lockControl anhand des gespeicherten controllType. */ public CardLockService create(CardLockEntity lock) { - return new CardLockService(lock, communityVerificationVoteRepository, communityVerificationRepository, - keyholderVerificationRepository, gameHistoryRepository, userRepository, - keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService, - keyholderTaskChoiceRepository, communityTaskVoteRepository, cardLockRepository); + CardLockService service = new CardLockService(lock, communityVerificationVoteRepository, + communityVerificationRepository, keyholderVerificationRepository, gameHistoryRepository, + userRepository, keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService, + keyholderTaskChoiceRepository, communityTaskVoteRepository, cardLockRepository, lockControlFactory); + + return service; } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java index 889b788..381da7a 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +import de.oaa.xxx.games.chastity.lockcontroll.LockControllType; import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.TaskListConverter; import de.oaa.xxx.games.chastity.tasks.TaskMode; @@ -51,6 +52,9 @@ public class BaseLockEntity { private Integer unlockCodeLength; @Column private String unlockCode; + @Enumerated(EnumType.STRING) + @Column(length = 20) + private LockControllType controllType; // --- Timing & Hygiene --- @Column diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockRepository.java index 63a3b60..e6e2598 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockRepository.java @@ -1,9 +1,11 @@ package de.oaa.xxx.games.chastity.common; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; public interface BaseLockRepository extends JpaRepository{ - + + Optional findByLockee(UUID userId); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java index dd30ab7..1031acf 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java @@ -46,6 +46,13 @@ public abstract class BaseLockService { protected final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; protected final CommunityTaskVoteRepository communityTaskVoteRepository; + /** Wird von Subklassen gesetzt; steuert wie das physische Schloss (neu) verriegelt wird. */ + protected de.oaa.xxx.games.chastity.lockcontroll.LockControl lockControl; + + public de.oaa.xxx.games.chastity.lockcontroll.LockControl getLockControl() { + return lockControl; + } + // ── Abstrakte Methoden ──────────────────────────────────────────────────── protected abstract BaseLockEntity getLock(); @@ -106,7 +113,13 @@ public abstract class BaseLockService { notification.setKeyholderUserId(lock.getKeyholder()); notification.setViolationTime(LocalDateTime.now()); notification.setOvertimeMinutes(overtime); + notification.setOpeningReason(de.oaa.xxx.games.chastity.unlock.TempOpeningReason.HYGIENE); keyholderNotificationRepository.save(notification); + userRepository.findById(lock.getKeyholder()).ifPresent(kh -> + sendMessage(lock.getLockee(), kh.getUserId(), + "Deine Lockee hat die Hygiene-Öffnung um " + overtime + " Minuten überschritten.", + "/keyholder.html?lockId=" + lock.getLockId(), + de.oaa.xxx.social.entity.MessageCause.GAME_STATE)); } protected void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, @@ -193,7 +206,16 @@ public abstract class BaseLockService { lock.setLastHygineOpening(now); lock.setTempOpeningDuration(null); lock.setTempOpeningTime(null); - String code = CodeCreator.createNumeric(lock.getUnlockCodeLength()); + if (lockControl != null + && lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) { + // TTLock/Trust: lockControl.lock() setzt PIN am Gerät (oder tut nichts bei Trust). + // Kein Software-Code notwendig. + lockControl.lock(); + saveLock(); + return lock.getUnlockCode() != null ? lock.getUnlockCode() : ""; + } + // UNLOCK_CODE (oder kein lockControl): neuen numerischen Code generieren + String code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5); lock.setUnlockCode(code); saveLock(); return code; @@ -240,6 +262,10 @@ public abstract class BaseLockService { LOGGER.debug("Unlocked at {}", lock.getUnlockTime()); saveLock(); + if (lockControl != null) { + lockControl.cleanup(); + } + if (valid) { long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes(); GameHistoryEntity entry = new GameHistoryEntity(); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderNotificationEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderNotificationEntity.java index eefb43a..ee635a8 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderNotificationEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderNotificationEntity.java @@ -37,6 +37,7 @@ public class KeyholderNotificationEntity { @Column(nullable = false) private boolean notifiedKeyholder = false; + @Enumerated(EnumType.STRING) @Column(nullable = false) private TempOpeningReason openingReason; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControl.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControl.java index 7efc575..1db9495 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControl.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControl.java @@ -13,4 +13,6 @@ public abstract class LockControl { public abstract boolean unlock(); public abstract boolean lock(); + + public abstract boolean cleanup(); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControlFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControlFactory.java new file mode 100644 index 0000000..144ef63 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/LockControlFactory.java @@ -0,0 +1,42 @@ +package de.oaa.xxx.games.chastity.lockcontroll; + +import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; +import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository; +import de.oaa.xxx.games.chastity.ttlock.TTAuthService; +import de.oaa.xxx.games.chastity.ttlock.TTLockService; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class LockControlFactory { + + private final TTAuthService ttAuthService; + private final TTLockService ttLockService; + private final TTLockConfigRepository ttLockConfigRepository; + private final TTLockUserConfigRepository ttLockUserConfigRepository; + + public LockControlFactory(TTAuthService ttAuthService, + TTLockService ttLockService, + TTLockConfigRepository ttLockConfigRepository, + TTLockUserConfigRepository ttLockUserConfigRepository) { + this.ttAuthService = ttAuthService; + this.ttLockService = ttLockService; + this.ttLockConfigRepository = ttLockConfigRepository; + this.ttLockUserConfigRepository = ttLockUserConfigRepository; + } + + public LockControl create(LockControllType type, LockControlCallback callback, UUID lockeeId) { + return switch (type != null ? type : LockControllType.UNLOCK_CODE) { + case TRUST -> new TrustLockControl(); + case TTLOCK -> new TTLockControl( + ttAuthService, + ttLockService, + ttLockConfigRepository.findById(1L).orElse(null), + ttLockUserConfigRepository.findById(lockeeId).orElse(null), + ttLockUserConfigRepository, + callback); + case UNLOCK_CODE -> new UnlockcodeLockControl(callback); + }; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TTLockControl.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TTLockControl.java index 43a08a6..740a378 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TTLockControl.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TTLockControl.java @@ -1,29 +1,142 @@ package de.oaa.xxx.games.chastity.lockcontroll; +import de.oaa.xxx.games.chastity.common.CodeCreator; +import de.oaa.xxx.games.chastity.ttlock.TTAuthService; +import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity; +import de.oaa.xxx.games.chastity.ttlock.TTLockService; +import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigEntity; +import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class TTLockControl extends LockControl { - // private static final String BASE_URL = "https://euapi.ttlock.com/"; + private static final Logger LOGGER = LoggerFactory.getLogger(TTLockControl.class); - public TTLockControl() { - super(new NoInteractionCallback()); - } + private final TTAuthService ttAuthService; + private final TTLockService ttLockService; + private final TTLockConfigEntity adminConfig; + private final TTLockUserConfigEntity userConfig; + private final TTLockUserConfigRepository userConfigRepository; + + public TTLockControl(TTAuthService ttAuthService, + TTLockService ttLockService, + TTLockConfigEntity adminConfig, + TTLockUserConfigEntity userConfig, + TTLockUserConfigRepository userConfigRepository, + LockControlCallback callback) { + super(callback); + this.ttAuthService = ttAuthService; + this.ttLockService = ttLockService; + this.adminConfig = adminConfig; + this.userConfig = userConfig; + this.userConfigRepository = userConfigRepository; + } + + @Override + public boolean init() { + return true; + } + + @Override + public boolean lock() { + if (!isConfigValid()) { + LOGGER.warn("TTLock-Konfiguration unvollständig – lock() übersprungen"); + return false; + } + try { + String token = getToken(); + if (token == null) { + LOGGER.error("TTLock: Kein Access Token erhalten"); + return false; + } + + // Neuen PIN erstellen – Länge aus Callback, mindestens 4, maximal 9 (TTLock-Limit) + int pinLength = Math.min(9, Math.max(4, callback.getUnlockcodeLenght())); + String newPin = CodeCreator.createNumeric(pinLength); + Integer newPwdId = ttLockService.addCustomPasscode( + adminConfig.getClientId(), token, + userConfig.getLockId(), newPin); + + if (newPwdId == null) { + LOGGER.error("TTLock: Neuer PIN konnte nicht erstellt werden – alter PIN bleibt erhalten"); + return false; + } + callback.setUnlockCode(newPin); + + // Neuen PIN-ID speichern, dann alten PIN löschen + Integer oldPwdId = userConfig.getCurrentKeyboardPwdId(); + userConfig.setCurrentKeyboardPwdId(newPwdId); + userConfigRepository.save(userConfig); + LOGGER.info("TTLock: Neuer PIN gesetzt (pwdId={})", newPwdId); + + + if (oldPwdId != null) { + ttLockService.deleteCustomPasscode( + adminConfig.getClientId(), token, + userConfig.getLockId(), oldPwdId); + LOGGER.debug("TTLock: Alter PIN {} gelöscht", oldPwdId); + } + + return true; + } catch (Exception e) { + LOGGER.error("TTLock lock() fehlgeschlagen: {}", e.getMessage(), e); + return false; + } + } + + /** + * Löscht den aktuellen PIN vom Schloss, sodass es entsperrt bleibt. + */ + @Override + public boolean unlock() { + if (!isConfigValid() || userConfig.getCurrentKeyboardPwdId() == null) { + return true; // Kein PIN gesetzt – nichts zu tun + } + try { + String token = getToken(); + if (token == null) return false; + + ttLockService.deleteCustomPasscode( + adminConfig.getClientId(), token, + userConfig.getLockId(), userConfig.getCurrentKeyboardPwdId()); + + userConfig.setCurrentKeyboardPwdId(null); + userConfigRepository.save(userConfig); + LOGGER.info("TTLock: PIN gelöscht (Entsperrung)"); + return true; + } catch (Exception e) { + LOGGER.error("TTLock unlock() fehlgeschlagen: {}", e.getMessage(), e); + return false; + } + } + + private String getToken() { + return ttAuthService.getAccessToken( + adminConfig.getClientId(), + adminConfig.getClientSecret(), + userConfig.getUsername(), + userConfig.getPasswordMd5()); + } + + private boolean isConfigValid() { + return adminConfig != null + && adminConfig.getClientId() != null + && adminConfig.getClientSecret() != null + && userConfig != null + && userConfig.getUsername() != null + && userConfig.getPasswordMd5() != null + && userConfig.getLockId() != null; + } @Override - public boolean init() { - // TODO Auto-generated method stub - return false; + public boolean cleanup() { + String token = getToken(); + if (token == null) { + LOGGER.error("TTLock: Kein Access Token erhalten"); + return false; + } + ttLockService.findAndDeleteLocksByName(adminConfig.getClientId(), token, userConfig.getLockId()); + return true; } - - @Override - public boolean unlock() { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean lock() { - // TODO Auto-generated method stub - return false; - } - } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TrustLockControl.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TrustLockControl.java index 583bb8c..944e908 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TrustLockControl.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/TrustLockControl.java @@ -19,5 +19,10 @@ public class TrustLockControl extends LockControl { @Override public boolean lock() { return true; + } + + @Override + public boolean cleanup() { + return true; } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/UnlockcodeLockControl.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/UnlockcodeLockControl.java index e052505..82ff99f 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/UnlockcodeLockControl.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockcontroll/UnlockcodeLockControl.java @@ -24,4 +24,9 @@ public class UnlockcodeLockControl extends LockControl { callback.setUnlockCode(code); return true; } + + @Override + public boolean cleanup() { + return true; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java index 9b4e38a..9a3b05e 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java @@ -37,6 +37,7 @@ import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository; import de.oaa.xxx.games.chastity.lockcontroll.LockControllType; +import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository; import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry; @@ -57,6 +58,7 @@ public class TimeLockController { private final TimeLockServiceFactory timeLockServiceFactory; private final CommunityVerificationRepository verificationRepository; private final CommunityVerificationVoteRepository verificationVoteRepository; + private final SubscriptionLimitService subscriptionLimitService; public TimeLockController(TimeLockRepository timeLockRepository, TimeLockTemplateRepository templateRepository, @@ -66,7 +68,8 @@ public class TimeLockController { SystemMessageService systemMessageService, TimeLockServiceFactory timeLockServiceFactory, CommunityVerificationRepository verificationRepository, - CommunityVerificationVoteRepository verificationVoteRepository) { + CommunityVerificationVoteRepository verificationVoteRepository, + SubscriptionLimitService subscriptionLimitService) { this.timeLockRepository = timeLockRepository; this.templateRepository = templateRepository; this.userRepository = userRepository; @@ -76,6 +79,7 @@ public class TimeLockController { this.timeLockServiceFactory = timeLockServiceFactory; this.verificationRepository = verificationRepository; this.verificationVoteRepository = verificationVoteRepository; + this.subscriptionLimitService = subscriptionLimitService; } // ── Erstellen ──────────────────────────────────────────────────────────────── @@ -140,6 +144,11 @@ public class TimeLockController { if (timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE; + if (controllType == LockControllType.TTLOCK && !subscriptionLimitService.hasActivePaidSubscription(myId)) { + return ResponseEntity.status(403).body(Map.of("error", "subscription_required")); + } + TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings( req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE, myId, req.keyholder(), req.testLock(), codeLen); @@ -410,6 +419,24 @@ public class TimeLockController { // ── Aufgabe erledigt ────────────────────────────────────────────────────────── + @PostMapping("/timelock/{lockId}/relock") + @Transactional + public ResponseEntity relock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = timeLockRepository.findById(lockId); + if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); + if (l.getControllType() != LockControllType.TTLOCK) return ResponseEntity.status(409).build(); + + var lc = timeLockServiceFactory.create(l).getLockControl(); + if (lc != null) lc.lock(); + return ResponseEntity.noContent().build(); + } + @PostMapping("/timelock/{lockId}/task/done") @Transactional public ResponseEntity taskDone(@PathVariable UUID lockId, Principal principal) { @@ -564,7 +591,7 @@ public class TimeLockController { verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId())); verificationRepository.deleteAll(verifications); invitationRepository.deleteByLockId(lockId); - timeLockRepository.deleteById(lockId); + timeLockRepository.deleteByLockId(lockId); return ResponseEntity.noContent().build(); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java index 5d40b59..b6f7cde 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java @@ -3,9 +3,14 @@ package de.oaa.xxx.games.chastity.timelock; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface TimeLockRepository extends JpaRepository { boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee); + @Modifying + @Query("DELETE FROM TimeLockEntity t WHERE t.lockId = :lockId") + void deleteByLockId(UUID lockId); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java index ed5055f..a41daa7 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java @@ -24,11 +24,8 @@ import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; -import de.oaa.xxx.games.chastity.lockcontroll.LockControl; import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback; -import de.oaa.xxx.games.chastity.lockcontroll.TTLockControl; -import de.oaa.xxx.games.chastity.lockcontroll.TrustLockControl; -import de.oaa.xxx.games.chastity.lockcontroll.UnlockcodeLockControl; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory; import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry; import de.oaa.xxx.games.chastity.tasks.TaskMode; import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; @@ -44,8 +41,9 @@ public class TimeLockService extends BaseLockService implements LockControlCallb private final TimeLockEntity lock; private final TimeLockRepository timeLockRepository; private final CommunityPilloryRepository pilloryRepository; + private final LockControlFactory lockControlFactory; - private LockControl lockControl; + // lockControl ist in BaseLockService als protected-Feld definiert public TimeLockService(TimeLockEntity lock, CommunityVerificationRepository verificationRepository, @@ -59,7 +57,8 @@ public class TimeLockService extends BaseLockService implements LockControlCallb CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository, UnlockCodeHistoryService unlockCodeHistoryService, - SystemMessageService systemMessageService) { + SystemMessageService systemMessageService, + LockControlFactory lockControlFactory) { super(verificationVoteRepository, verificationRepository, keyholderVerificationRepository, gameHistoryRepository, userRepository, keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService, @@ -67,6 +66,11 @@ public class TimeLockService extends BaseLockService implements LockControlCallb this.lock = lock; this.timeLockRepository = timeLockRepository; this.pilloryRepository = pilloryRepository; + this.lockControlFactory = lockControlFactory; + // lockControl aus Entity-Typ wiederherstellen (für bereits laufende Locks) + if (lock.getControllType() != null) { + this.lockControl = lockControlFactory.create(lock.getControllType(), this, lock.getLockee()); + } } // ── Abstract method implementations ────────────────────────────────────── @@ -111,11 +115,11 @@ public class TimeLockService extends BaseLockService implements LockControlCallb * generiert und das Lock bereits persistiert. */ public void init(TimeLockTemplateEntity template, TimeLockAdditionalSettings settings) { - switch (settings.controllType()) { - case TTLOCK -> lockControl = new TTLockControl(); - case TRUST -> lockControl = new TrustLockControl(); - case UNLOCK_CODE -> lockControl = new UnlockcodeLockControl(this); - } + de.oaa.xxx.games.chastity.lockcontroll.LockControllType type = + settings.controllType() != null ? settings.controllType() + : de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE; + lock.setControllType(type); + lockControl = lockControlFactory.create(type, this, settings.lockee()); LocalDateTime now = LocalDateTime.now(); lock.setStartTime(now); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java index f8f7e9f..7498303 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java @@ -9,6 +9,7 @@ import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; +import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; import de.oaa.xxx.games.history.GameHistoryRepository; import de.oaa.xxx.social.SystemMessageService; @@ -30,6 +31,7 @@ public class TimeLockServiceFactory { private final UnlockCodeHistoryService unlockCodeHistoryService; private final SystemMessageService systemMessageService; private CommunityVerificationVoteRepository communityVerificationVoteRepository; + private final LockControlFactory lockControlFactory; public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository, CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository, @@ -38,7 +40,8 @@ public class TimeLockServiceFactory { KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, KeyholderVerificationRepository keyholderVerificationRepository, CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository, - UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService) { + UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService, + LockControlFactory lockControlFactory) { this.communityVerificationVoteRepository = verificationVoteRepository; this.timeLockRepository = timeLockRepository; this.communityVerificationRepository = verificationRepository; @@ -51,15 +54,16 @@ public class TimeLockServiceFactory { this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.communityTaskVoteRepository = communityTaskVoteRepository; this.keyholderVerificationRepository = keyholderVerificationRepository; + this.lockControlFactory = lockControlFactory; } /** - * Erstellt eine neue CardLockService-Instanz für das gegebene Lock. + * Erstellt eine neue TimeLockService-Instanz für das gegebene Lock. */ public TimeLockService create(TimeLockEntity lock) { return new TimeLockService(lock, communityVerificationRepository, communityVerificationVoteRepository, timeLockRepository, gameHistoryRepository, userRepository, keyholderNotificationRepository, keyholderTaskChoiceRepository, keyholderVerificationRepository, communityTaskVoteRepository, - pilloryRepository, unlockCodeHistoryService, systemMessageService); + pilloryRepository, unlockCodeHistoryService, systemMessageService, lockControlFactory); } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTAuthService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTAuthService.java new file mode 100644 index 0000000..aaafdf9 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTAuthService.java @@ -0,0 +1,38 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import java.util.Map; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Service +public class TTAuthService { + + private final String AUTH_URL = "https://euapi.ttlock.com/oauth2/token"; + + @SuppressWarnings("unchecked") + public String getAccessToken(String clientId, String clientSecret, String username, String md5Password) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("client_id", clientId); + map.add("client_secret", clientSecret); + map.add("username", username); + map.add("password", md5Password); // MD5 Hash des TTLock-Passworts + map.add("grant_type", "password"); + + HttpEntity> request = new HttpEntity<>(map, headers); + + // Response parsen und access_token extrahieren + Map response = restTemplate.postForObject(AUTH_URL, request, Map.class); + return (String) response.get("access_token"); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockCallback.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockCallback.java new file mode 100644 index 0000000..565ac91 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockCallback.java @@ -0,0 +1,225 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.oaa.xxx.games.chastity.common.BaseLockEntity; +import de.oaa.xxx.games.chastity.common.BaseLockRepository; +import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity; +import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; +import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; +import de.oaa.xxx.social.SystemMessageService; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.user.UserRepository; +import lombok.Data; + +@RestController +@RequestMapping("/api/ttlock/callback") +public class TTLockCallback { + + private static final Logger LOGGER = LoggerFactory.getLogger(TTLockCallback.class); + + private static final int START_WINDOW_MINUTES = 5; + + private final TTLockUserConfigRepository ttLockUserConfigRepository; + private final BaseLockRepository baseLockRepository; + private final KeyholderNotificationRepository keyholderNotificationRepository; + private final UserRepository userRepository; + private final SystemMessageService systemMessageService; + + public TTLockCallback(TTLockUserConfigRepository ttLockUserConfigRepository, + BaseLockRepository baseLockRepository, + KeyholderNotificationRepository keyholderNotificationRepository, + UserRepository userRepository, + SystemMessageService systemMessageService) { + this.ttLockUserConfigRepository = ttLockUserConfigRepository; + this.baseLockRepository = baseLockRepository; + this.keyholderNotificationRepository = keyholderNotificationRepository; + this.userRepository = userRepository; + this.systemMessageService = systemMessageService; + } + + @GetMapping + public String test() { + return "OK"; + } + + @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public String handleCallback(@RequestParam Map allRequestParams) { + LOGGER.debug("Callback von TTLock erhalten, verarbeite..."); + try { + var wrapper = parse(allRequestParams); + if (Integer.valueOf(1).equals(wrapper.getNotifyType())) { + LOGGER.info("Lock {} wurde aufgeschlossen", wrapper.getLockId()); + checkUser(wrapper); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Uninteressantes ereignis: {}", wrapper); + } + } + return "1"; + } catch (Exception e) { + LOGGER.error("Fehler beim Verarbeiten des Callbacks", e); + return "0"; + } + } + + @Transactional + void checkUser(TTLockCallbackWrapper wrapper) { + var userOpt = ttLockUserConfigRepository.findByLockId(wrapper.getLockId()); + if (userOpt.isEmpty()) { + LOGGER.warn("TTLock-Öffnung für unbekanntes Lock {} – nicht in XXX-Sphere registriert", wrapper.getLockId()); + return; + } + + var lockOpt = baseLockRepository.findByLockee(userOpt.get().getUserId()); + if (lockOpt.isEmpty()) { + LOGGER.debug("Kein aktives Lock für Benutzer {} gefunden", userOpt.get().getUserId()); + return; + } + + var lock = lockOpt.get(); + + if (lock.getKeyholder() == null) { + LOGGER.debug("Lock {} hat keinen Keyholder – keine Berechtigungsprüfung notwendig", lock.getLockId()); + return; + } + + // Nur erfolgreiche Öffnungen prüfen + LockRecord record = wrapper.getRecords() != null && !wrapper.getRecords().isEmpty() + ? wrapper.getRecords().get(0) : null; + if (record != null && !Integer.valueOf(1).equals(record.getSuccess())) { + LOGGER.debug("Öffnungsversuch an Lock {} war nicht erfolgreich – ignoriere", lock.getLockId()); + return; + } + + if (isOpeningAuthorized(lock)) { + LOGGER.debug("Öffnung von Lock {} ist berechtigt", lock.getLockId()); + } else { + LOGGER.warn("Unerlaubte Öffnung von Lock {} erkannt – benachrichtige Keyholder {}", + lock.getLockId(), lock.getKeyholder()); + notifyKeyholder(lock); + } + } + + /** + * Prüft, ob eine Öffnung des Schlosses zu diesem Zeitpunkt berechtigt ist. + * Berechtigt sind: + *

+ */ + private boolean isOpeningAuthorized(BaseLockEntity lock) { + // Spiel beendet + if (lock.getUnlockTime() != null) return true; + + // Keyholder hat Entsperrung genehmigt + if (lock.isKeyholderRequestedUnlock()) return true; + + // Aktive temporäre Öffnung (Hygiene, Karte, Aufgabe) + if (lock.getTempOpeningTime() != null) return true; + + // Start-Fenster: Anwender hat beim Lock-Start den Code erhalten und + // hat das Schloss physisch noch nicht übergeben (Relock läuft gerade) + if (lock.getStartTime() != null + && ChronoUnit.MINUTES.between(lock.getStartTime(), LocalDateTime.now()) <= START_WINDOW_MINUTES) { + return true; + } + + return false; + } + + private void notifyKeyholder(BaseLockEntity lock) { + KeyholderNotificationEntity notification = new KeyholderNotificationEntity(); + notification.setLockId(lock.getLockId()); + notification.setLockeeId(lock.getLockee()); + notification.setKeyholderUserId(lock.getKeyholder()); + notification.setViolationTime(LocalDateTime.now()); + notification.setOvertimeMinutes(0); + notification.setNotifiedKeyholder(false); + notification.setOpeningReason(TempOpeningReason.TTLOCK_UNAUTHORIZED); + keyholderNotificationRepository.save(notification); + userRepository.findById(lock.getKeyholder()).ifPresent(kh -> + systemMessageService.send(lock.getLockee(), kh.getUserId(), + "Deine Lockee hat ihr Schloss unerlaubt geöffnet!", + "/keyholder.html?lockId=" + lock.getLockId(), + MessageCause.GAME_STATE)); + } + + private TTLockCallbackWrapper parse(Map params) { + ObjectMapper mapper = new ObjectMapper(); + TTLockCallbackWrapper wrapper = new TTLockCallbackWrapper(); + + try { + if (params.containsKey("lockId")) + wrapper.setLockId(Integer.parseInt(params.get("lockId"))); + if (params.containsKey("notifyType")) + wrapper.setNotifyType(Integer.parseInt(params.get("notifyType"))); + wrapper.setLockMac(params.get("lockMac")); + + String recordsJson = params.get("records"); + if (recordsJson != null && !recordsJson.isEmpty()) { + List recordList = mapper.readValue(recordsJson, new TypeReference>() { + }); + wrapper.setRecords(recordList); + } + } catch (Exception e) { + System.err.println("Fehler beim Parsen des TTLock Callbacks: " + e.getMessage()); + } + + return wrapper; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public class TTLockCallbackWrapper { + private Integer lockId; + private Integer notifyType; + private String lockMac; + private List records; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class LockRecord { + private Integer lockId; + private Integer electricQuantity; + private Long serverDate; + private Integer recordType; + private Integer success; + private String lockMac; + private String keyboardPwd; + private Long lockDate; + private String username; + + public LocalDateTime getLockDateTime() { + if (this.lockDate == null || this.lockDate == 0) return null; + return LocalDateTime.ofInstant( + Instant.ofEpochMilli(this.lockDate), + ZoneId.systemDefault() + ); + } + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigEntity.java new file mode 100644 index 0000000..d000d2b --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigEntity.java @@ -0,0 +1,26 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "ttlock_config") +public class TTLockConfigEntity { + + /** Singleton-Zeile – immer ID 1 */ + @Id + @Column + private Long id = 1L; + + @Column(length = 100) + private String clientId; + + @Column(length = 100) + private String clientSecret; + + @Column(length = 200) + private String baseUrl; +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigRepository.java new file mode 100644 index 0000000..84e7f4f --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockConfigRepository.java @@ -0,0 +1,5 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TTLockConfigRepository extends JpaRepository {} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockService.java new file mode 100644 index 0000000..ecccc87 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockService.java @@ -0,0 +1,169 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import java.util.Collections; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Service +public class TTLockService { + + private final RestTemplate restTemplate; + + private static final String UNLOCK_CODE_NAME = "xxx-unlock-code"; + + public TTLockService() { + restTemplate = new RestTemplate(); + + } + + public TTLockDetailResponse getLockDetail(String clientId, String accessToken, int lockId) { + String url = UriComponentsBuilder.fromUriString("https://euapi.ttlock.com/v3/lock/detail") + .queryParam("clientId", clientId).queryParam("accessToken", accessToken).queryParam("lockId", lockId) + .queryParam("date", System.currentTimeMillis()).toUriString(); + + try { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + TTLockDetailResponse response = restTemplate.getForObject(url, TTLockDetailResponse.class); + System.out.println(response); + return response; + } catch (Exception e) { + System.err.println("Fehler beim Abrufen der Details: " + e.getMessage()); + return null; + } + } + + public Integer addCustomPasscode(String clientId, String accessToken, int lockId, String pin) { + + String url = "https://euapi.ttlock.com/v3/keyboardPwd/add"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("clientId", clientId); + map.add("accessToken", accessToken); + map.add("lockId", String.valueOf(lockId)); + map.add("keyboardPwd", pin); // Der 4-9 stellige PIN + map.add("keyboardPwdName", UNLOCK_CODE_NAME); + map.add("addType", "2"); + map.add("keyboardPwdType", "2"); + map.add("date", String.valueOf(System.currentTimeMillis())); + + HttpEntity> request = new HttpEntity<>(map, headers); + + try { + ResponseEntity response = restTemplate.postForEntity(url, request, + TTLockAddPasscodeResponse.class); + if (response.getBody() != null && response.getBody().isSuccess()) { + return response.getBody().getKeyboardPwdId(); + } else { + System.out.println("Fehler von TTLock: " + response.getBody().getErrmsg()); + return null; + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public String deleteCustomPasscode(String clientId, String accessToken, int lockId, int keyboardPwdId) { + String url = "https://euapi.ttlock.com/v3/keyboardPwd/delete"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("clientId", clientId); + map.add("accessToken", accessToken); + map.add("lockId", String.valueOf(lockId)); + map.add("keyboardPwdId", String.valueOf(keyboardPwdId)); + map.add("deleteType", "2"); + map.add("date", String.valueOf(System.currentTimeMillis())); + + HttpEntity> request = new HttpEntity<>(map, headers); + + try { + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + return response.getBody(); + } catch (Exception e) { + return "{\"errcode\":-1, \"errmsg\":\"" + e.getMessage() + "\"}"; + } + } + + public void findAndDeleteLocksByName(String clientId, String accessToken, int lockId) { + ObjectMapper mapper = new ObjectMapper(); + + String listUrl = UriComponentsBuilder.fromUriString("https://euapi.ttlock.com/v3/lock/listKeyboardPwd") + .queryParam("clientId", clientId) + .queryParam("accessToken", accessToken) + .queryParam("lockId", lockId) + .queryParam("pageNo", 1) + .queryParam("pageSize", 100) + .queryParam("date", System.currentTimeMillis()).toUriString(); + + try { + String response = restTemplate.getForObject(listUrl, String.class); + JsonNode root = mapper.readTree(response); + JsonNode list = root.get("list"); + + if (list != null && list.isArray()) { + for (JsonNode unlockcode : list) { + String name = unlockcode.get("keyboardPwdName").asText(); + int passwordId = unlockcode.get("keyboardPwdId").asInt(); + + if (name.equalsIgnoreCase(UNLOCK_CODE_NAME)) { + deleteCustomPasscode(clientId, accessToken, lockId, passwordId); + } + } + } + } catch (Exception e) { + System.err.println("Fehler beim Massenlöschen: " + e.getMessage()); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + @Data + public static class TTLockDetailResponse { + private int errcode; + private String errmsg; + private String lockName; + private String lockAlias; + private int lockId; + private int electricQuantity; + private String modelNum; + private String featureValue; + private String adminPwd; + private String state; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TTLockAddPasscodeResponse { + private int errcode; + private String errmsg; + private Integer keyboardPwdId; // Die ID des neuen Pins in der Cloud + + public boolean isSuccess() { + return errcode == 0; + } + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockTest.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockTest.java new file mode 100644 index 0000000..da137aa --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockTest.java @@ -0,0 +1,56 @@ +package de.oaa.xxx.games.chastity.ttlock; + +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.games.chastity.ttlock.TTLockService.TTLockDetailResponse; + +@RestController +@RequestMapping("/ttlock") +public class TTLockTest { + + private final TTAuthService auth; + private final TTLockService lock; + + private String clientId = "6e5077a84b6a4e1ba0fb6a8da21c6417"; + private String clientSecret = "a2c1d68c7905d52584fc29028937db11"; + private String username= "mario.stoermer@proton.me"; + private String password = "knall666.Halla"; + private int lockId = 30158446; + + public TTLockTest(TTAuthService auth, TTLockService lock) { + this.auth = auth; + this.lock = lock; + } + + @GetMapping("/details") + public ResponseEntity details() { + String md5Hex = org.apache.commons.codec.digest.DigestUtils.md5Hex(password).toLowerCase(); + String token = auth.getAccessToken(clientId, clientSecret, username, md5Hex); + return ResponseEntity.ok(lock.getLockDetail(clientId, token, lockId)); + } + + @GetMapping("/add/{pin}") + public ResponseEntity add(@PathVariable String pin) { + String md5Hex = org.apache.commons.codec.digest.DigestUtils.md5Hex(password).toLowerCase(); + String token = auth.getAccessToken(clientId, clientSecret, username, md5Hex); + return ResponseEntity.ok(lock.addCustomPasscode(clientId, token, lockId, pin)); + } + + @GetMapping("/delete/{id}") + public ResponseEntity remove(@PathVariable Integer id) { + String md5Hex = org.apache.commons.codec.digest.DigestUtils.md5Hex(password).toLowerCase(); + String token = auth.getAccessToken(clientId, clientSecret, username, md5Hex); + return ResponseEntity.ok(lock.deleteCustomPasscode(clientId, token, lockId, id)); + } + + @GetMapping("/delete/all") + public void removeAll() { + String md5Hex = org.apache.commons.codec.digest.DigestUtils.md5Hex(password).toLowerCase(); + String token = auth.getAccessToken(clientId, clientSecret, username, md5Hex); + lock.findAndDeleteLocksByName(clientId, token, lockId); + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigEntity.java new file mode 100644 index 0000000..e00d970 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigEntity.java @@ -0,0 +1,32 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@Table(name = "ttlock_user_config") +public class TTLockUserConfigEntity { + + @Id + @Column + private UUID userId; + + @Column(length = 200) + private String username; + + /** MD5-Hex des TTLock-Passworts (so wie TTAuthService es erwartet) */ + @Column(length = 32) + private String passwordMd5; + + @Column + private Integer lockId; + + /** ID des aktuell gesetzten PINs auf dem Schloss – wird zum Löschen beim nächsten lock() benötigt */ + @Column + private Integer currentKeyboardPwdId; +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigRepository.java new file mode 100644 index 0000000..5fcedb8 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/TTLockUserConfigRepository.java @@ -0,0 +1,11 @@ +package de.oaa.xxx.games.chastity.ttlock; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TTLockUserConfigRepository extends JpaRepository { + + Optional findByLockId(Integer lockId); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/unlocktypes b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/unlocktypes new file mode 100644 index 0000000..0d5b861 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/unlocktypes @@ -0,0 +1,121 @@ +1-unlock by app + +4-unlock by passcode + +5-Rise the lock (for parking lock) + +6-Lower the lock (for parking lock) + +7-unlock by IC card + +8-unlock by fingerprint + +9-unlock by wrist strap + +10-unlock by Mechanical key + +11-lock by app + +12-unlock by gateway + +29-apply some force on the Lock + +30-Door sensor closed + +31-Door sensor open + +32-open from inside + +33-lock by fingerprint + +34-lock by passcode + +35-lock by IC card + +36-lock by Mechanical key + +37-Use APP button to control the lock (rise, fall, stop, lock), mostly used for roller shutter door + +42-received new local mail + +43-received new other cities' mail + +44-Tamper alert + +45-Auto Lock + +46-unlock by unlock key + +47-lock by lock key + +48-System locked ( Caused by, for example: Using INVALID Passcode/Fingerprint/Card several times) + +49-unlock by hotel card + +50-Unlocked due to the high temperature + +51-Try to unlock with a deleted card + +52-Dead lock with APP + +53-Dead lock with passcode + +54-The car left (for parking lock) + +55-Use remote control lock or unlock lock + +57-Unlock with QR code success + +58-Unlock with QR code failed, it's expired + +59-Double locked + +60-Cancel double lock + +61-Lock with QR code success + +62-Lock with QR code failed, the lock is double locked + +63-Auto unlock at passage mode + +64-Door unclosed alarm + +65-Failed to unlock + +66-Failed to lock + +67-Face unlock success + +68-Face unlock failed - door locked from inside + +69-Lock with face + +71-Face unlock failed - expired or ineffective + +75-Unlocked by App granting + +76-Unlocked by remote granting + +77-Dual authentication Bluetooth unlock verification success, waiting for second user + +78-Dual authentication password unlock verification success, waiting for second user + +79-Dual authentication fingerprint unlock verification success, waiting for second user + +80-Dual authentication IC card unlock verification success, waiting for second user + +81-Dual authentication face card unlock verification success, waiting for second user + +82-Dual authentication wireless key unlock verification success, waiting for second user + +83-Dual authentication palm vein unlock verification success, waiting for second user + +84-Palm vein unlock success + +85-Palm vein unlock success + +86-Lock with palm vein + +88-Palm vein unlock failed - expired or ineffective + +92-Administrator password to unlock \ No newline at end of file diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java index 184aecb..f121f78 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java @@ -1,5 +1,5 @@ package de.oaa.xxx.games.chastity.unlock; public enum TempOpeningReason { - HYGIENE, CARD, TASK; + HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/subscription/UserSubscriptionRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/subscription/UserSubscriptionRepository.java index c9f2888..19dafab 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/subscription/UserSubscriptionRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/subscription/UserSubscriptionRepository.java @@ -3,6 +3,7 @@ package de.oaa.xxx.subscription; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -10,4 +11,6 @@ public interface UserSubscriptionRepository extends JpaRepository findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc( UUID userId, LocalDate today); + + List findByValidUntilGreaterThanEqualOrderByValidUntilDesc(LocalDate today); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java index 6cc9f8b..31bc3b0 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java @@ -25,11 +25,21 @@ 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.games.chastity.common.BaseLockRepository; +import de.oaa.xxx.games.chastity.common.CodeCreator; +import de.oaa.xxx.games.chastity.ttlock.TTAuthService; +import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; +import de.oaa.xxx.games.chastity.ttlock.TTLockService; +import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigEntity; +import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository; 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; +import org.springframework.util.DigestUtils; + +import java.nio.charset.StandardCharsets; @RestController @RequestMapping("/user") @@ -42,22 +52,39 @@ public class UserController { private final NotificationPreferenceRepository notificationPreferenceRepository; private final BdsmDefaultsRepository bdsmDefaultsRepository; private final UserService userService; + private final TTLockUserConfigRepository ttLockUserConfigRepository; + private final TTLockConfigRepository ttLockConfigRepository; + private final TTAuthService ttAuthService; + private final TTLockService ttLockService; + private final BaseLockRepository baseLockRepository; public UserController(UserRepository userRepository, RegistrationRepository registrationRepository, NotificationPreferenceRepository notificationPreferenceRepository, BdsmDefaultsRepository bdsmDefaultsRepository, - UserService userService) { + UserService userService, + TTLockUserConfigRepository ttLockUserConfigRepository, + TTLockConfigRepository ttLockConfigRepository, + TTAuthService ttAuthService, + TTLockService ttLockService, + BaseLockRepository baseLockRepository) { this.userRepository = userRepository; this.registrationRepository = registrationRepository; this.notificationPreferenceRepository = notificationPreferenceRepository; this.bdsmDefaultsRepository = bdsmDefaultsRepository; this.userService = userService; + this.ttLockUserConfigRepository = ttLockUserConfigRepository; + this.ttLockConfigRepository = ttLockConfigRepository; + this.ttAuthService = ttAuthService; + this.ttLockService = ttLockService; + this.baseLockRepository = baseLockRepository; } record ProfilePictureRequest(String picture, String pictureHq) {} record NameChangeRequest(String name) {} record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {} + record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId) {} + record TtlockUserConfigRequest(String username, String password, Integer lockId) {} record ProfileRequest(Integer groesse, Integer gewicht, Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} record PrivacyRequest( @@ -270,6 +297,132 @@ public class UserController { .build(); } + // ── TTLock-Account ──────────────────────────────────────────────────────── + + @GetMapping("/me/ttlock") + public ResponseEntity getTtlockUserConfig(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userOpt.get().getUserId()) + .orElse(new TTLockUserConfigEntity()); + return ResponseEntity.ok(new TtlockUserConfigDto( + cfg.getUsername(), + cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(), + cfg.getLockId() + )); + } + + @PutMapping("/me/ttlock") + public ResponseEntity saveTtlockUserConfig(@RequestBody TtlockUserConfigRequest body, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId) + .orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; }); + cfg.setUsername(body.username()); + if (body.password() != null && !body.password().isBlank()) { + cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8))); + } + cfg.setLockId(body.lockId()); + ttLockUserConfigRepository.save(cfg); + return ResponseEntity.ok().build(); + } + + @GetMapping("/me/ttlock/test") + public ResponseEntity> testTtlockConnection(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); + if (userCfg == null || userCfg.getUsername() == null || userCfg.getPasswordMd5() == null || userCfg.getLockId() == null) { + return ResponseEntity.badRequest().body(Map.of("error", "ttlock_not_configured")); + } + var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); + if (adminCfg == null || adminCfg.getClientId() == null || adminCfg.getClientSecret() == null) { + return ResponseEntity.badRequest().body(Map.of("error", "admin_config_missing")); + } + + String token = ttAuthService.getAccessToken( + adminCfg.getClientId(), adminCfg.getClientSecret(), + userCfg.getUsername(), userCfg.getPasswordMd5()); + if (token == null) { + return ResponseEntity.status(502).body(Map.of("error", "auth_failed")); + } + + TTLockService.TTLockDetailResponse detail = ttLockService.getLockDetail( + adminCfg.getClientId(), token, userCfg.getLockId()); + if (detail == null || detail.getErrcode() != 0) { + String msg = detail != null ? detail.getErrmsg() : "Keine Antwort"; + return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg)); + } + + Map result = new LinkedHashMap<>(); + result.put("lockId", detail.getLockId()); + result.put("lockName", detail.getLockName()); + result.put("lockAlias", detail.getLockAlias()); + result.put("modelNum", detail.getModelNum()); + result.put("electricQuantity", detail.getElectricQuantity()); + result.put("state", detail.getState()); + return ResponseEntity.ok(result); + } + + @PostMapping("/me/ttlock/open") + public ResponseEntity> ttlockOpen(Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); + if (userCfg == null || userCfg.getUsername() == null || userCfg.getPasswordMd5() == null || userCfg.getLockId() == null) { + return ResponseEntity.badRequest().body(Map.of("error", "ttlock_not_configured")); + } + var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); + if (adminCfg == null || adminCfg.getClientId() == null) { + return ResponseEntity.badRequest().body(Map.of("error", "admin_config_missing")); + } + + var activeLock = baseLockRepository.findByLockee(userId); + if (activeLock.isPresent() && activeLock.get().getUnlockTime() == null) { + return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + } + + String token = ttAuthService.getAccessToken( + adminCfg.getClientId(), adminCfg.getClientSecret(), + userCfg.getUsername(), userCfg.getPasswordMd5()); + if (token == null) { + return ResponseEntity.status(502).body(Map.of("error", "auth_failed")); + } + + String pin = CodeCreator.createNumeric(6); + Integer pwdId = ttLockService.addCustomPasscode(adminCfg.getClientId(), token, userCfg.getLockId(), pin); + if (pwdId == null) { + return ResponseEntity.status(502).body(Map.of("error", "passcode_failed")); + } + + return ResponseEntity.ok(Map.of("pin", pin, "keyboardPwdId", pwdId)); + } + + @DeleteMapping("/me/ttlock/open/{keyboardPwdId}") + public ResponseEntity ttlockCloseOpen(@PathVariable int keyboardPwdId, Principal principal) { + var userOpt = userRepository.findByEmail(principal.getName()); + if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID userId = userOpt.get().getUserId(); + + var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); + if (userCfg == null || userCfg.getLockId() == null) return ResponseEntity.badRequest().build(); + var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); + if (adminCfg == null) return ResponseEntity.badRequest().build(); + + String token = ttAuthService.getAccessToken( + adminCfg.getClientId(), adminCfg.getClientSecret(), + userCfg.getUsername(), userCfg.getPasswordMd5()); + if (token == null) return ResponseEntity.status(502).build(); + + ttLockService.deleteCustomPasscode(adminCfg.getClientId(), token, userCfg.getLockId(), keyboardPwdId); + return ResponseEntity.ok().build(); + } + @PostMapping public ResponseEntity userAnlegen(@RequestBody Registration registration) { try { diff --git a/xxxthegame/src/main/resources/application.properties b/xxxthegame/src/main/resources/application.properties index 3a16508..e48a358 100644 --- a/xxxthegame/src/main/resources/application.properties +++ b/xxxthegame/src/main/resources/application.properties @@ -56,6 +56,9 @@ server.servlet.context-path=/ server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=5s +# Jackson – Datumsformat als ISO-8601 String statt numerischem Array +spring.jackson.serialization.write-dates-as-timestamps=false + # Multipart upload spring.servlet.multipart.max-file-size=20MB spring.servlet.multipart.max-request-size=20MB diff --git a/xxxthegame/src/main/resources/static/admin.html b/xxxthegame/src/main/resources/static/admin.html index 5e6ad76..8a5113b 100644 --- a/xxxthegame/src/main/resources/static/admin.html +++ b/xxxthegame/src/main/resources/static/admin.html @@ -438,6 +438,8 @@ + + @@ -536,6 +538,91 @@ + +
+ + +
+
+

Aktive Abonnements

+ +
+
+ + + + + + + + + + + + +
BenutzerTypGestartetGültig bis
Laden…
+
+
+ + +
+
+

Abonnement verschenken

+
+
+

+ Suche einen Benutzer und schenke ihm 1 Monat Premium. Hat der Benutzer bereits ein + aktives Abo, wird die Laufzeit um 1 Monat verlängert. +

+ + +
+ + + +
+ + + + +
+ +
+ +
+
+
+
+ + +
+
+
+

TTLock-Konfiguration

+
+
+

API-Zugangsdaten

+ + + + + + +
+
+ + +
+
+
+
+ @@ -554,12 +641,12 @@ async function init() { if (!r.ok) { window.location.href = '/userhome.html'; return; } const admin = await r.json(); if (admin.rolle === 'SUPERADMIN') { - document.querySelectorAll('.superadmin-only').forEach(el => el.style.display = ''); + document.querySelectorAll('.superadmin-only').forEach(el => el.classList.remove('superadmin-only')); } loadMeldungen(); loadAdminGruppen(); loadAdminToys(); - if (admin.rolle === 'SUPERADMIN') loadAdmins(); + if (admin.rolle === 'SUPERADMIN') { loadAdmins(); loadTtlockConfig(); loadAllSubscriptions(); } const _savedAdminTab = localStorage.getItem('tab_admin'); if (_savedAdminTab) { @@ -1716,6 +1803,204 @@ document.getElementById('bildImportStart').addEventListener('click', async () => cancelBtn.disabled = false; cancelBtn.textContent = 'Schließen'; }); +// ── TTLock-Konfiguration ────────────────────────────────────────────────── + +async function loadTtlockConfig() { + const r = await fetch('/admin/ttlock'); if (!r.ok) return; + const cfg = await r.json(); + document.getElementById('ttClientId').value = cfg.clientId || ''; + document.getElementById('ttClientSecret').value = cfg.clientSecret || ''; + document.getElementById('ttBaseUrl').value = cfg.baseUrl || ''; + document.getElementById('ttSaveError').textContent = ''; +} + +async function saveTtlockConfig() { + const err = document.getElementById('ttSaveError'); + err.textContent = ''; + const body = { + clientId: document.getElementById('ttClientId').value.trim(), + clientSecret: document.getElementById('ttClientSecret').value.trim(), + baseUrl: document.getElementById('ttBaseUrl').value.trim() + }; + const r = await fetch('/admin/ttlock', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (r.ok) { + err.style.color = 'var(--color-success, #2ecc71)'; + err.textContent = 'Gespeichert.'; + await loadTtlockConfig(); + setTimeout(() => { err.textContent = ''; err.style.color = ''; }, 3000); + } else { + err.style.color = ''; + err.textContent = 'Fehler beim Speichern.'; + } +} + +// ── Abonnement-Hilfsfunktionen ──────────────────────────────────────────── + +function parseLocalDate(d) { + if (!d) return null; + if (Array.isArray(d)) return new Date(d[0], d[1] - 1, d[2]); + return new Date(d); +} + +function formatLocalDate(d) { + const parsed = parseLocalDate(d); + if (!parsed || isNaN(parsed)) return '–'; + return parsed.toLocaleDateString('de-DE'); +} + +async function loadAllSubscriptions() { + const tbody = document.getElementById('aboOverviewBody'); + if (!tbody) return; + tbody.innerHTML = 'Laden…'; + const r = await fetch('/admin/subscriptions'); + if (!r.ok) { + tbody.innerHTML = 'Fehler beim Laden.'; + return; + } + const list = await r.json(); + if (list.length === 0) { + tbody.innerHTML = 'Keine aktiven Abonnements.'; + return; + } + tbody.innerHTML = list.map(s => { + const until = formatLocalDate(s.validUntil); + const since = formatLocalDate(s.subscribedAt); + return ` + ${escAdminHtml(s.userName)} + ${escAdminHtml(s.subscriptionType)} + ${since} + ${until} + `; + }).join(''); +} + +// ── Abonnement verschenken ──────────────────────────────────────────────── + +(function() { + let aboSearchTimer = null; + + document.getElementById('aboSearchInput').addEventListener('input', function() { + clearTimeout(aboSearchTimer); + document.getElementById('aboUserId').value = ''; + document.getElementById('aboStatus').style.display = 'none'; + setAboBtn(false); + const q = this.value.trim(); + if (q.length < 2) { closeAboDropdown(); return; } + aboSearchTimer = setTimeout(() => searchAboUsers(q), 280); + }); + + document.getElementById('aboSearchInput').addEventListener('blur', function() { + setTimeout(closeAboDropdown, 150); + }); +})(); + +async function searchAboUsers(q) { + const r = await fetch('/admin/users/search/all?q=' + encodeURIComponent(q)); + if (!r.ok) return; + const users = await r.json(); + const dd = document.getElementById('aboDropdown'); + dd.innerHTML = ''; + if (users.length === 0) { + dd.innerHTML = '
Keine Treffer.
'; + } else { + users.forEach(u => { + const div = document.createElement('div'); + div.style.cssText = 'padding:.55rem .85rem;cursor:pointer;font-size:.9rem;color:var(--color-text);'; + div.textContent = u.name; + div.addEventListener('mouseover', () => div.style.background = 'var(--color-secondary)'); + div.addEventListener('mouseout', () => div.style.background = ''); + div.addEventListener('mousedown', e => { + e.preventDefault(); + document.getElementById('aboSearchInput').value = u.name; + document.getElementById('aboUserId').value = u.userId; + closeAboDropdown(); + loadAboStatus(u.userId); + }); + dd.appendChild(div); + }); + } + dd.style.display = ''; +} + +function closeAboDropdown() { + document.getElementById('aboDropdown').style.display = 'none'; +} + +async function loadAboStatus(userId) { + const statusEl = document.getElementById('aboStatus'); + const errEl = document.getElementById('aboError'); + errEl.textContent = ''; + statusEl.style.display = 'none'; + setAboBtn(false); + + const r = await fetch('/admin/subscriptions/user/' + userId); + if (!r.ok) { errEl.textContent = 'Fehler beim Laden des Abo-Status.'; return; } + const s = await r.json(); + + let html = `${escAdminHtml(s.userName)}
`; + if (s.validUntil) { + const until = formatLocalDate(s.validUntil); + const isActive = parseLocalDate(s.validUntil) >= new Date(); + html += `Aktuelles Abo: ${s.subscriptionType}`; + html += ` – gültig bis ${until}`; + if (isActive) html += `
Nach dem Schenken: gültig bis ${addOneMonth(s.validUntil)}`; + } else { + html += `Kein aktives Abonnement (STANDARD)`; + } + statusEl.innerHTML = html; + statusEl.style.display = ''; + setAboBtn(true); +} + +function addOneMonth(dateVal) { + const d = parseLocalDate(dateVal); + if (!d || isNaN(d)) return '–'; + d.setMonth(d.getMonth() + 1); + return d.toLocaleDateString('de-DE'); +} + +function setAboBtn(enabled) { + const btn = document.getElementById('aboBtnGift'); + btn.disabled = !enabled; + btn.style.opacity = enabled ? '' : '.45'; + btn.style.cursor = enabled ? '' : 'not-allowed'; +} + +async function giftSubscription() { + const userId = document.getElementById('aboUserId').value; + const errEl = document.getElementById('aboError'); + if (!userId) return; + errEl.textContent = ''; + setAboBtn(false); + + const r = await fetch('/admin/subscriptions/gift', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId }) + }); + if (r.ok) { + const s = await r.json(); + const until = formatLocalDate(s.validUntil); + errEl.style.color = '#2ecc71'; + errEl.textContent = `✅ 1 Monat Premium geschenkt – gültig bis ${until}`; + setTimeout(() => { errEl.textContent = ''; errEl.style.color = ''; }, 5000); + loadAboStatus(userId); + loadAllSubscriptions(); + } else { + errEl.style.color = ''; + errEl.textContent = 'Fehler beim Verschenken.'; + setAboBtn(true); + } +} + +function escAdminHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>'); +} + init(); diff --git a/xxxthegame/src/main/resources/static/aufgaben.html b/xxxthegame/src/main/resources/static/aufgaben.html index ef75be0..fc24e11 100644 --- a/xxxthegame/src/main/resources/static/aufgaben.html +++ b/xxxthegame/src/main/resources/static/aufgaben.html @@ -328,6 +328,17 @@ + + + + +
+
+ 🔒 TTLock + +
+
+

+ Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern. +

+ +
+
Benutzername (E-Mail)
+ +
+ +
+
Passwort
+ +
+ +
+
Lock-ID
+ +
+ +
+
+ + + +
+ + + + + +
+
+
+ + + + + + @@ -1020,7 +1082,139 @@ loadNotifications(); loadBdsmDefaults(); loadSubscription(); + loadTtlockUserConfig(); openSectionFromHash(); + + // ── TTLock ────────────────────────────────────────────────────────────── + + async function loadTtlockUserConfig() { + const r = await fetch('/user/me/ttlock'); + if (!r.ok) return; + const cfg = await r.json(); + document.getElementById('ttl-username').value = cfg.username || ''; + document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : ''; + document.getElementById('ttl-password').value = ''; + document.getElementById('ttl-pw-hint').textContent = + cfg.passwordSet ? '(gesetzt – leer lassen zum Beibehalten)' : '(noch nicht gesetzt)'; + } + + async function testTtlockConnection() { + const btn = document.getElementById('ttl-test-btn'); + const result = document.getElementById('ttl-test-result'); + btn.disabled = true; + btn.textContent = '⏳ Teste…'; + result.style.display = 'none'; + + try { + const r = await fetch('/user/me/ttlock/test'); + const data = await r.json(); + + if (r.ok) { + const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '–'; + const state = data.state || '–'; + result.style.background = 'rgba(39,174,96,0.08)'; + result.style.borderColor = '#27ae60'; + result.innerHTML = ` +
✅ Verbindung erfolgreich
+ + + + + + +
Name${escHtml(data.lockName || '–')}
Alias${escHtml(data.lockAlias || '–')}
Modell${escHtml(data.modelNum || '–')}
Akku${escHtml(battery)}
Status${escHtml(state)}
`; + } else { + const msgs = { + ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.', + admin_config_missing: 'Systemkonfiguration für TTLock fehlt (Admin).', + auth_failed: 'Anmeldung bei TTLock fehlgeschlagen – Zugangsdaten prüfen.', + lock_detail_failed: `Lock-Abfrage fehlgeschlagen: ${data.message || ''}`, + }; + result.style.background = 'rgba(231,76,60,0.08)'; + result.style.borderColor = '#e74c3c'; + result.innerHTML = `
❌ ${escHtml(msgs[data.error] || 'Unbekannter Fehler.')}
`; + } + } catch { + result.style.background = 'rgba(231,76,60,0.08)'; + result.style.borderColor = '#e74c3c'; + result.innerHTML = `
❌ Netzwerkfehler.
`; + } + + result.style.display = ''; + btn.disabled = false; + btn.textContent = '🔌 Verbindung testen'; + } + + function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>'); + } + + async function ttlockOpenOnce() { + const btn = document.getElementById('ttl-open-btn'); + const errEl = document.getElementById('ttl-open-error'); + btn.disabled = true; + btn.textContent = '⏳ …'; + errEl.textContent = ''; + + try { + const r = await fetch('/user/me/ttlock/open', { method: 'POST' }); + const data = await r.json(); + if (!r.ok) { + const msgs = { + ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.', + admin_config_missing: 'Systemkonfiguration fehlt (Admin).', + auth_failed: 'Anmeldung bei TTLock fehlgeschlagen.', + passcode_failed: 'PIN konnte nicht am Schloss angelegt werden.', + active_lock_exists: 'Du befindest dich in einem aktiven Lock – manuelles Öffnen ist nicht möglich.', + }; + errEl.textContent = msgs[data.error] || 'Unbekannter Fehler.'; + return; + } + + const pwdId = data.keyboardPwdId; + document.getElementById('ttl-open-pin').textContent = data.pin; + + const modal = document.getElementById('ttlOpenModal'); + modal.classList.add('visible'); + + const okBtn = document.getElementById('ttl-open-ok-btn'); + const onOk = async () => { + okBtn.removeEventListener('click', onOk); + okBtn.disabled = true; + modal.classList.remove('visible'); + await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' }); + okBtn.disabled = false; + }; + okBtn.addEventListener('click', onOk); + } catch { + errEl.textContent = 'Netzwerkfehler.'; + } finally { + btn.disabled = false; + btn.textContent = '🔓 Öffnen'; + } + } + + async function saveTtlockUserConfig() { + const errEl = document.getElementById('ttl-error'); + errEl.textContent = ''; + const lockIdVal = document.getElementById('ttl-lockid').value.trim(); + const body = { + username: document.getElementById('ttl-username').value.trim(), + password: document.getElementById('ttl-password').value, + lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null + }; + const r = await fetch('/user/me/ttlock', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (r.ok) { + showToast(); + await loadTtlockUserConfig(); + } else { + errEl.textContent = 'Fehler beim Speichern.'; + } + } diff --git a/xxxthegame/src/main/resources/static/keyholder.html b/xxxthegame/src/main/resources/static/keyholder.html index 930bc4d..feca302 100644 --- a/xxxthegame/src/main/resources/static/keyholder.html +++ b/xxxthegame/src/main/resources/static/keyholder.html @@ -593,15 +593,19 @@ html += ``; } - // Hygiene-Verletzungen + // Verletzungen if (d.hygieneViolations && d.hygieneViolations.length > 0) { html += `
-
Hygiene-Verletzungen (letzte ${d.hygieneViolations.length})
`; +
Verletzungen (letzte ${d.hygieneViolations.length})
`; d.hygieneViolations.forEach(v => { const dt = new Date(v.time).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }); - html += `
- ${dt} · +${v.overtimeMinutes} Min. Überschreitung -
`; + let label; + if (v.openingReason === 'TTLOCK_UNAUTHORIZED') { + label = 'Unerlaubte Öffnung'; + } else { + label = `+${v.overtimeMinutes} Min. Hygiene-Überschreitung`; + } + html += `
${dt} · ${label}
`; }); html += `
`; } @@ -1346,8 +1350,18 @@ } catch(e) { console.error(e); } } - // Initial laden - loadLocks(); + // Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen + loadLocks().then(() => { + const params = new URLSearchParams(window.location.search); + const preselect = params.get('lockId'); + if (preselect) { + const card = document.querySelector(`[data-lock-id="${preselect}"]`); + if (card) { + card.scrollIntoView({ behavior: 'smooth', block: 'start' }); + toggleLock(card, preselect); + } + } + }); // Automatische Aktualisierung alle 60 Sekunden für geöffnete Lock-Details setInterval(() => { diff --git a/xxxthegame/src/main/resources/static/neulock.html b/xxxthegame/src/main/resources/static/neulock.html index a7a1905..451db80 100644 --- a/xxxthegame/src/main/resources/static/neulock.html +++ b/xxxthegame/src/main/resources/static/neulock.html @@ -128,6 +128,31 @@ .tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; } .tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; } + /* LockControl-Auswahl */ + .lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; } + .lockcontrol-option { + display: flex; align-items: flex-start; gap: 0.7rem; + padding: 0.7rem 0.85rem; + border: 1px solid var(--color-secondary); border-radius: 8px; + cursor: pointer; transition: border-color 0.15s; + } + .lockcontrol-option:hover:not(.lc-disabled) { border-color: var(--color-primary); } + .lockcontrol-option.lc-selected { border-color: var(--color-primary); background: rgba(var(--color-primary-rgb, 180,80,255),0.06); } + .lockcontrol-option.lc-disabled { opacity: 0.55; cursor: not-allowed; } + .lockcontrol-option input[type="radio"] { + margin-top: 0.15rem; flex-shrink: 0; + accent-color: var(--color-primary); width: 1rem; height: 1rem; cursor: pointer; + } + .lockcontrol-option.lc-disabled input[type="radio"] { cursor: not-allowed; } + .lc-label { font-size: 0.9rem; font-weight: 600; color: var(--color-text); } + .lc-desc { font-size: 0.78rem; color: var(--color-muted); margin-top: 0.15rem; } + .lc-badge { + display: inline-block; font-size: 0.68rem; font-weight: 700; + padding: 0.15em 0.5em; border-radius: 4px; + background: var(--color-primary); color: #fff; + margin-left: 0.4rem; vertical-align: middle; letter-spacing: 0.03em; + } + /* Unlock-Code-Modal */ .modal-overlay { display: none; position: fixed; inset: 0; z-index: 500; @@ -228,6 +253,34 @@
Die Dauer wird beim Lock-Start zufällig aus dem Bereich der Vorlage gewählt.
+ +
+ +
+ + + +
+
+
@@ -277,16 +330,18 @@ - +