Anbindung an TTLock umgesetzt

This commit is contained in:
2026-03-24 22:51:27 +01:00
parent b85245717c
commit 528ea89bc4
48 changed files with 4577 additions and 1965 deletions

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

@@ -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<List<UserSearchDto>> searchAllUsers(
@RequestParam String q, Principal principal) {
requireSuperAdmin(principal);
if (q == null || q.isBlank()) return ResponseEntity.ok(List.of());
List<UserEntity> 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<List<SubscriptionStatusDto>> 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<SubscriptionStatusDto> 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<SubscriptionStatusDto> 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<TtlockConfigDto> 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<Void> 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();
}
}

View File

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

View File

@@ -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<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
List<Task> 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<Void> 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<Void> 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<String, Object> result = new HashMap<>();

View File

@@ -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<CardLockEntity, UUID> {
@Modifying
@Query("DELETE FROM CardLockEntity c WHERE c.lockId = :lockId")
void deleteByLockId(UUID lockId);
}

View File

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

View File

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

View File

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

View File

@@ -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<BaseLockEntity, UUID>{
Optional<BaseLockEntity> findByLockee(UUID userId);
}

View File

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

View File

@@ -37,6 +37,7 @@ public class KeyholderNotificationEntity {
@Column(nullable = false)
private boolean notifiedKeyholder = false;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TempOpeningReason openingReason;
}

View File

@@ -13,4 +13,6 @@ public abstract class LockControl {
public abstract boolean unlock();
public abstract boolean lock();
public abstract boolean cleanup();
}

View File

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

View File

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

View File

@@ -19,5 +19,10 @@ public class TrustLockControl extends LockControl {
@Override
public boolean lock() {
return true;
}
@Override
public boolean cleanup() {
return true;
}
}

View File

@@ -24,4 +24,9 @@ public class UnlockcodeLockControl extends LockControl {
callback.setUnlockCode(code);
return true;
}
@Override
public boolean cleanup() {
return true;
}
}

View File

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

View File

@@ -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<TimeLockEntity, UUID> {
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
@Modifying
@Query("DELETE FROM TimeLockEntity t WHERE t.lockId = :lockId")
void deleteByLockId(UUID lockId);
}

View File

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

View File

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

View File

@@ -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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
// Response parsen und access_token extrahieren
Map<String, Object> response = restTemplate.postForObject(AUTH_URL, request, Map.class);
return (String) response.get("access_token");
}
}

View File

@@ -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<String, String> 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:
* <ul>
* <li>Das Spiel ist bereits beendet (unlockTime gesetzt)</li>
* <li>Der Keyholder hat die Entsperrung genehmigt (keyholderRequestedUnlock)</li>
* <li>Es läuft gerade eine temporäre Öffnung (Hygiene, Karte, Aufgabe)</li>
* <li>Das Lock wurde vor Kurzem gestartet der Anwender hat den Startcode erhalten
* und die Übergabe an das physische Schloss ist noch nicht abgeschlossen</li>
* </ul>
*/
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<String, String> 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<LockRecord> recordList = mapper.readValue(recordsJson, new TypeReference<List<LockRecord>>() {
});
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<LockRecord> 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()
);
}
}
}

View File

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

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.games.chastity.ttlock;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TTLockConfigRepository extends JpaRepository<TTLockConfigEntity, Long> {}

View File

@@ -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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
try {
ResponseEntity<TTLockAddPasscodeResponse> 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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
try {
ResponseEntity<String> 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;
}
}
}

View File

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

View File

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

View File

@@ -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<TTLockUserConfigEntity, UUID> {
Optional<TTLockUserConfigEntity> findByLockId(Integer lockId);
}

View File

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

View File

@@ -1,5 +1,5 @@
package de.oaa.xxx.games.chastity.unlock;
public enum TempOpeningReason {
HYGIENE, CARD, TASK;
HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED;
}

View File

@@ -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<UserSubscripti
Optional<UserSubscriptionEntity> findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(
UUID userId, LocalDate today);
List<UserSubscriptionEntity> findByValidUntilGreaterThanEqualOrderByValidUntilDesc(LocalDate today);
}

View File

@@ -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<TtlockUserConfigDto> 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<Void> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<Void> 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<Void> userAnlegen(@RequestBody Registration registration) {
try {

View File

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

View File

@@ -438,6 +438,8 @@
<button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button>
<button class="tab-btn" data-tab="toys">Toys</button>
<button class="tab-btn superadmin-only" data-tab="admins">Admins</button>
<button class="tab-btn superadmin-only" data-tab="abonnements">Abonnements</button>
<button class="tab-btn superadmin-only" data-tab="schnittstellen">Schnittstellen</button>
</div>
<!-- ── Meldungen ── -->
@@ -536,6 +538,91 @@
</div>
</div>
<!-- ── Abonnements (nur Superadmin) ── -->
<div class="tab-panel superadmin-only" id="panel-abonnements">
<!-- Aktive Abonnements Übersicht -->
<div class="section">
<div class="section-header">
<h2 class="section-title">Aktive Abonnements</h2>
<button class="btn-action" onclick="loadAllSubscriptions()">Aktualisieren</button>
</div>
<div class="table-card">
<table class="data-table" id="aboOverviewTable">
<thead>
<tr>
<th>Benutzer</th>
<th>Typ</th>
<th>Gestartet</th>
<th>Gültig bis</th>
</tr>
</thead>
<tbody id="aboOverviewBody">
<tr><td colspan="4" style="color:var(--color-muted);font-style:italic;">Laden…</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Abonnement verschenken -->
<div class="section">
<div class="section-header">
<h2 class="section-title">Abonnement verschenken</h2>
</div>
<div class="form-section">
<p style="font-size:0.85rem;color:var(--color-muted);margin:0 0 1.25rem 0;">
Suche einen Benutzer und schenke ihm 1 Monat Premium. Hat der Benutzer bereits ein
aktives Abo, wird die Laufzeit um 1 Monat verlängert.
</p>
<label style="display:block;font-size:.8rem;color:#aaa;margin-bottom:.3rem;">Benutzer suchen</label>
<div style="position:relative;" id="aboComboWrap">
<input type="text" id="aboSearchInput" placeholder="Name eingeben…"
autocomplete="off"
style="width:100%;box-sizing:border-box;padding:.6rem .85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:.95rem;outline:none;transition:border-color .2s;"
onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'">
<input type="hidden" id="aboUserId">
<div id="aboDropdown" style="display:none;position:absolute;top:calc(100% + 3px);left:0;right:0;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:8px;max-height:200px;overflow-y:auto;z-index:200;box-shadow:0 4px 16px rgba(0,0,0,0.25);"></div>
</div>
<!-- Aktueller Abo-Status -->
<div id="aboStatus" style="display:none;margin-top:1rem;padding:.75rem 1rem;background:var(--color-secondary);border-radius:8px;font-size:.88rem;line-height:1.6;"></div>
<div id="aboError" style="color:var(--color-primary);font-size:.82rem;margin-top:.75rem;min-height:1.1em;"></div>
<div style="margin-top:1.25rem;">
<button id="aboBtnGift" class="btn-add" onclick="giftSubscription()" disabled
style="opacity:.45;cursor:not-allowed;">
🎁 1 Monat Premium schenken
</button>
</div>
</div>
</div>
</div>
<!-- ── Schnittstellen (nur Superadmin) ── -->
<div class="tab-panel superadmin-only" id="panel-schnittstellen">
<div class="section">
<div class="section-header">
<h2 class="section-title">TTLock-Konfiguration</h2>
</div>
<div class="form-section">
<h3>API-Zugangsdaten</h3>
<label for="ttClientId" style="display:block;font-size:.8rem;color:#aaa;margin-top:.75rem;margin-bottom:.3rem;">Client ID</label>
<input type="text" id="ttClientId" placeholder="Client ID" style="width:100%;box-sizing:border-box;padding:.6rem .85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:.95rem;outline:none;transition:border-color .2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'">
<label for="ttClientSecret" style="display:block;font-size:.8rem;color:#aaa;margin-top:1rem;margin-bottom:.3rem;">Client Secret</label>
<input type="text" id="ttClientSecret" placeholder="Client Secret" style="width:100%;box-sizing:border-box;padding:.6rem .85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:.95rem;outline:none;transition:border-color .2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'">
<label for="ttBaseUrl" style="display:block;font-size:.8rem;color:#aaa;margin-top:1rem;margin-bottom:.3rem;">Base URL</label>
<input type="text" id="ttBaseUrl" placeholder="https://euapi.ttlock.com/v3/" style="width:100%;box-sizing:border-box;padding:.6rem .85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:.95rem;outline:none;transition:border-color .2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'">
<div id="ttSaveError" style="color:var(--color-primary);font-size:.82rem;margin-top:.75rem;min-height:1.1em;"></div>
<div style="margin-top:1.25rem;display:flex;gap:.75rem;justify-content:flex-end;">
<button class="btn-action" onclick="loadTtlockConfig()">Zurücksetzen</button>
<button class="btn-add" onclick="saveTtlockConfig()">Speichern</button>
</div>
</div>
</div>
</div>
</div><!-- .content -->
</div><!-- .main -->
@@ -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 = '<tr><td colspan="4" style="color:var(--color-muted);font-style:italic;">Laden…</td></tr>';
const r = await fetch('/admin/subscriptions');
if (!r.ok) {
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--color-muted);">Fehler beim Laden.</td></tr>';
return;
}
const list = await r.json();
if (list.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--color-muted);font-style:italic;">Keine aktiven Abonnements.</td></tr>';
return;
}
tbody.innerHTML = list.map(s => {
const until = formatLocalDate(s.validUntil);
const since = formatLocalDate(s.subscribedAt);
return `<tr>
<td>${escAdminHtml(s.userName)}</td>
<td><strong>${escAdminHtml(s.subscriptionType)}</strong></td>
<td>${since}</td>
<td>${until}</td>
</tr>`;
}).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 = '<div style="padding:.55rem .85rem;font-size:.85rem;color:var(--color-muted);font-style:italic;">Keine Treffer.</div>';
} 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 = `<strong>${escAdminHtml(s.userName)}</strong><br>`;
if (s.validUntil) {
const until = formatLocalDate(s.validUntil);
const isActive = parseLocalDate(s.validUntil) >= new Date();
html += `Aktuelles Abo: <strong style="color:${isActive ? '#2ecc71' : 'var(--color-muted)'};">${s.subscriptionType}</strong>`;
html += ` gültig bis <strong>${until}</strong>`;
if (isActive) html += `<br><span style="font-size:.8rem;color:var(--color-muted);">Nach dem Schenken: gültig bis <strong>${addOneMonth(s.validUntil)}</strong></span>`;
} 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
init();
</script>
</body>

View File

@@ -328,6 +328,17 @@
<body class="app">
<!-- Gruppe-Modal -->
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2 id="confirmModalTitle">Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Abbrechen</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Löschen</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="gruppeModal">
<div class="modal">
<h2 id="modalTitle">Neue Aufgabengruppe</h2>
@@ -1112,7 +1123,7 @@
// ── Delete ──
document.getElementById('deleteBtn').addEventListener('click', () => {
if (!selectedGruppeId || selectedGruppeType !== 'user') return;
if (!confirm('Aufgabengruppe und alle enthaltenen Aufgaben, Strafen und Zeitstrafen wirklich löschen?')) return;
openConfirmModal('Aufgabengruppe und alle enthaltenen Aufgaben, Strafen und Zeitstrafen wirklich löschen?', () => {
const btn = document.getElementById('deleteBtn');
btn.disabled = true;
fetch(`/gruppe/${selectedGruppeId}`, { method: 'DELETE' })
@@ -1129,6 +1140,7 @@
}
})
.catch(() => { document.getElementById('userActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
});
// ── Copy ──
@@ -1600,10 +1612,26 @@
if (publishModal.classList.contains('open')) { closePublishModal(); return; }
if (gruppeModal.classList.contains('open')) { closeGruppeModal(); return; }
if (itemModal.classList.contains('open')) { closeItemModal(); return; }
if (confirmModal.classList.contains('open')) { closeConfirmModal(); return; }
});
const confirmModal = document.getElementById('confirmModal');
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
confirmModal.addEventListener('click', e => { if (e.target === confirmModal) closeConfirmModal(); });
function closeConfirmModal() { confirmModal.classList.remove('open'); }
function openConfirmModal(text, onConfirm) {
document.getElementById('confirmModalText').textContent = text;
const okBtn = document.getElementById('confirmModalOk');
const handler = () => { okBtn.removeEventListener('click', handler); closeConfirmModal(); onConfirm(); };
okBtn.removeEventListener('click', handler);
okBtn.addEventListener('click', handler);
confirmModal.classList.add('open');
}
</script>
<script src="/js/icons.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
</body>
</html>

View File

@@ -122,6 +122,9 @@
align-items: center;
}
.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.notif-col-check input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
@@ -574,6 +577,65 @@
</div>
</div>
<!-- TTLock -->
<div class="settings-section" id="sec-ttlock">
<div class="settings-section-header" onclick="toggleSection('sec-ttlock')">
<span class="settings-section-title">🔒 TTLock</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
</p>
<div class="spiel-field">
<div class="settings-row-label">Benutzername (E-Mail)</div>
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
style="margin-top:0.3rem;" autocomplete="off">
</div>
<div class="spiel-field">
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
style="margin-top:0.3rem;" autocomplete="new-password">
</div>
<div class="spiel-field">
<div class="settings-row-label">Lock-ID</div>
<input type="number" id="ttl-lockid" placeholder="z.B. 30158446"
style="margin-top:0.3rem;max-width:220px;-moz-appearance:textfield;" min="0"
class="no-spinner">
</div>
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
<button onclick="ttlockOpenOnce()" id="ttl-open-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔓 Öffnen</button>
</div>
<!-- Test-Ergebnis -->
<div id="ttl-test-result" style="display:none;margin-top:1rem;padding:0.85rem 1rem;border-radius:8px;border:1px solid var(--color-secondary);font-size:0.85rem;"></div>
<!-- Öffnen-Ergebnis -->
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
</div>
</div>
</div>
</div>
<!-- TTLock Öffnen Modal -->
<div class="modal-backdrop" id="ttlOpenModal">
<div class="modal" style="max-width:380px;text-align:center;">
<h2>🔓 Schloss öffnen</h2>
<p style="color:var(--color-muted);font-size:0.85rem;margin-bottom:1rem;">Gib diesen PIN an deinem TTLock-Schloss ein:</p>
<div id="ttl-open-pin" style="font-size:2.2rem;font-weight:700;letter-spacing:0.25em;color:var(--color-primary);margin:0.5rem 0 1.5rem 0;"></div>
<p style="font-size:0.8rem;color:var(--color-muted);margin-bottom:1.25rem;">Nach „OK" wird dieser Code gelöscht und kann nicht mehr verwendet werden.</p>
<div class="modal-actions" style="justify-content:center;">
<button id="ttl-open-ok-btn" class="btn-save" style="min-width:120px;">OK</button>
</div>
</div>
</div>
@@ -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 = `
<div style="font-weight:700;color:#27ae60;margin-bottom:0.5rem;">✅ Verbindung erfolgreich</div>
<table style="border-collapse:collapse;width:100%;">
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;width:40%;">Name</td><td>${escHtml(data.lockName || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Alias</td><td>${escHtml(data.lockAlias || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Modell</td><td>${escHtml(data.modelNum || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Akku</td><td>${escHtml(battery)}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Status</td><td>${escHtml(state)}</td></tr>
</table>`;
} 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 = `<div style="color:#e74c3c;">❌ ${escHtml(msgs[data.error] || 'Unbekannter Fehler.')}</div>`;
}
} catch {
result.style.background = 'rgba(231,76,60,0.08)';
result.style.borderColor = '#e74c3c';
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
}
result.style.display = '';
btn.disabled = false;
btn.textContent = '🔌 Verbindung testen';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
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.';
}
}
</script>
</body>
</html>

View File

@@ -593,15 +593,19 @@
html += `</div>`;
}
// Hygiene-Verletzungen
// Verletzungen
if (d.hygieneViolations && d.hygieneViolations.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Hygiene-Verletzungen (letzte ${d.hygieneViolations.length})</div>`;
<div class="detail-section-title">Verletzungen (letzte ${d.hygieneViolations.length})</div>`;
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 += `<div class="violation-item">
${dt} · <span style="color:var(--color-primary);font-weight:600;">+${v.overtimeMinutes} Min. Überschreitung</span>
</div>`;
let label;
if (v.openingReason === 'TTLOCK_UNAUTHORIZED') {
label = '<span style="color:var(--color-primary);font-weight:600;">Unerlaubte Öffnung</span>';
} else {
label = `<span style="color:var(--color-primary);font-weight:600;">+${v.overtimeMinutes} Min. Hygiene-Überschreitung</span>`;
}
html += `<div class="violation-item">${dt} · ${label}</div>`;
});
html += `</div>`;
}
@@ -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(() => {

View File

@@ -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 @@
<div class="form-hint">Die Dauer wird beim Lock-Start zufällig aus dem Bereich der Vorlage gewählt.</div>
</div>
<!-- LockControl-Auswahl -->
<div class="form-row" id="rowLockControl">
<label>Schloss-Steuerung</label>
<div class="lockcontrol-options">
<label class="lockcontrol-option lc-selected" id="lcOptUnlockCode" onclick="selectLockControl('UNLOCK_CODE')">
<input type="radio" name="lockControl" value="UNLOCK_CODE" checked>
<div>
<div class="lc-label">🔢 Unlock-Code</div>
<div class="lc-desc">Ein numerischer Code wird generiert, den du in deinen Tresor einstellst.</div>
</div>
</label>
<label class="lockcontrol-option" id="lcOptTrust" onclick="selectLockControl('TRUST')">
<input type="radio" name="lockControl" value="TRUST">
<div>
<div class="lc-label">🤝 Trust</div>
<div class="lc-desc">Kein technisches Schloss du vertraust dir selbst oder deiner Keyholder*in.</div>
</div>
</label>
<label class="lockcontrol-option lc-disabled" id="lcOptTtlock" onclick="selectLockControl('TTLOCK')">
<input type="radio" name="lockControl" value="TTLOCK" disabled>
<div>
<div class="lc-label">📱 TTLock <span class="lc-badge" id="lcTtlockBadge">ABO</span></div>
<div class="lc-desc" id="lcTtlockDesc">Steuert ein TTLock-Smartschloss direkt über die App-Integration. Erfordert ein aktives Abonnement.</div>
</div>
</label>
</div>
</div>
<div class="form-row" id="rowUnlockCodeLines">
<label for="unlockCodeLines">Anzahl Ziffern des Entsperrcodes</label>
<div class="inline-number">
@@ -277,16 +330,18 @@
<script src="/js/card-defs.js"></script>
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let myUserId = null;
let myUserName = null;
let allFriends = [];
let allTemplates = []; // combined; each entry has _type: 'cardlock'|'timelock'
let myUserId = null;
let myUserName = null;
let allFriends = [];
let allTemplates = []; // combined; each entry has _type: 'cardlock'|'timelock'
let selectedTemplate = null;
let comboActiveIdx = -1;
let comboActiveIdx = -1;
let selectedLockControl = 'UNLOCK_CODE';
let hasPaidSubscription = false;
// ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
@@ -294,18 +349,29 @@
myUserId = user.userId;
myUserName = user.name;
// Templates laden Pflicht (beide Typen parallel)
// Subscription + Templates parallel laden
try {
const [cardTpls, timeTpls] = await Promise.all([
const [cardTpls, timeTpls, subData] = await Promise.all([
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
fetch('/timelock/templates').then(r => r.ok ? r.json() : [])
fetch('/timelock/templates').then(r => r.ok ? r.json() : []),
fetch('/subscription/me').then(r => r.ok ? r.json() : null)
]);
allTemplates = [
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
];
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
} catch { allTemplates = []; }
if (hasPaidSubscription) {
const opt = document.getElementById('lcOptTtlock');
opt.classList.remove('lc-disabled');
opt.querySelector('input').disabled = false;
document.getElementById('lcTtlockBadge').style.display = 'none';
document.getElementById('lcTtlockDesc').textContent =
'Steuert ein TTLock-Smartschloss direkt über die App-Integration.';
}
if (allTemplates.length === 0) {
document.querySelector('.content').innerHTML = `
<div style="text-align:center;padding:3rem 1rem;">
@@ -443,22 +509,20 @@
khHidden.value = myUserId;
khInput.readOnly = true;
khInput.style.opacity = '0.6';
document.getElementById('rowUnlockCodeLines').style.display = 'none';
document.getElementById('rowTestLock').style.display = 'none';
document.getElementById('rowDetailsVisible').style.display = '';
} else {
khInput.readOnly = false;
khInput.style.opacity = '';
if (!khHidden.value) khInput.value = '';
document.getElementById('rowUnlockCodeLines').style.display = '';
document.getElementById('rowTestLock').style.display = '';
document.getElementById('rowDetailsVisible').style.display = 'none';
}
updateCodeLinesVisibility();
}
// Self-Lock-Felder beim Start ausblenden (werden durch onLockeeChanged gesetzt)
document.getElementById('rowUnlockCodeLines').style.display = '';
document.getElementById('rowTestLock').style.display = '';
document.getElementById('rowTestLock').style.display = '';
// ── Keyholder-Combobox ──
function setupKeyholderCombo() {
@@ -516,6 +580,43 @@
return parts.join(' ') || '0 Min.';
}
// ── LockControl-Auswahl ──
function selectLockControl(type) {
const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' };
if (type === 'TTLOCK' && !hasPaidSubscription) return;
selectedLockControl = type;
Object.entries(ids).forEach(([t, id]) => {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('lc-selected', t === type);
el.querySelector('input').checked = (t === type);
});
updateCodeLinesVisibility();
}
function updateCodeLinesVisibility() {
const show = selectedLockControl === 'UNLOCK_CODE' || selectedLockControl === 'TTLOCK';
const lockeeIsFriend = document.getElementById('lockeeValue').value !== myUserId
&& !!document.getElementById('lockeeValue').value;
document.getElementById('rowUnlockCodeLines').style.display = (show && !lockeeIsFriend) ? '' : 'none';
// Label je nach Typ anpassen
const label = document.querySelector('#rowUnlockCodeLines > label');
if (label) {
label.textContent = selectedLockControl === 'TTLOCK'
? 'PIN-Länge (49 Ziffern)'
: 'Anzahl Ziffern des Entsperrcodes';
}
// Für TTLock: min=4, max=9; Standard: min=1, max=20
const input = document.getElementById('unlockCodeLines');
if (selectedLockControl === 'TTLOCK') {
input.min = 4; input.max = 9;
if (parseInt(input.value) < 4) input.value = 6;
if (parseInt(input.value) > 9) input.value = 9;
} else {
input.min = 1; input.max = 20;
}
}
// ── Zeitpicker ──
function tpChange(prefix, delta, seg) {
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
@@ -548,6 +649,13 @@
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showActiveLockError() {
const el = document.getElementById('errorMsg');
el.innerHTML = 'Du befindest dich bereits in einem aktiven Lock. '
+ '<a href="/meine-locks.html" style="color:inherit;text-decoration:underline;">Zum aktiven Lock</a>';
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function setFieldError(rowId, msg) {
const row = document.getElementById(rowId);
if (!row) return;
@@ -607,6 +715,7 @@
keyholder: isFriendLockee ? null : (keyholderVal || null),
testLock: isTestLock,
unlockCodeLength: unlockCodeLen,
controllType: selectedLockControl,
};
} else {
// CardLock
@@ -633,6 +742,7 @@
unlockCodeLines: unlockCodeLen,
requiresVerification: t.requiresVerification,
testLock: isTestLock,
controllType: selectedLockControl,
};
}
@@ -643,14 +753,15 @@
});
if (!res.ok) {
if (res.status === 409) {
const data = await res.json().catch(() => ({}));
if (data.error === 'active_lock_exists')
showError('Du hast bereits ein aktives Lock als Lockee. Erst das bestehende Lock beenden, bevor ein neues gestartet werden kann.');
else
showError('Fehler beim Erstellen des Locks.');
const errData = await res.json().catch(() => ({}));
if (res.status === 409 && errData.error === 'active_lock_exists') {
showActiveLockError();
} else if (res.status === 403 && errData.error === 'subscription_required') {
showError('TTLock erfordert ein aktives Abonnement.');
} else if (res.status === 400) {
showError('Ungültige Eingabe. Bitte alle Felder prüfen.');
} else {
showError(res.status === 400 ? 'Ungültige Eingabe. Bitte alle Felder prüfen.' : 'Fehler beim Erstellen des Locks.');
showError('Fehler beim Erstellen des Locks.');
}
return;
}
@@ -658,11 +769,45 @@
const data = await res.json();
if (data.lockeeInvitationSent) {
window.location.href = '/einladungen.html?tab=gesendet';
} else if (!data.unlockCode) {
// Trust: kein Code, direkt weiter
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
window.location.href = (isTimeLock ? '/activetimelock.html' : '/activelock.html')
+ '?lockId=' + data.lockId + (data.keyholderPending ? '&keyholderPending=1' : '');
} else if (selectedLockControl === 'TTLOCK') {
showTtlockStartModal(data.unlockCode, data.lockId, data.keyholderPending);
} else {
showUnlockCodeModal(data.unlockCode, data.lockId, data.keyholderPending);
}
}
// ── TTLock-Startmodal (kein Scramble, stattdessen Relock im Hintergrund) ──
function showTtlockStartModal(code, lockId, keyholderPending) {
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
const lockType = isTimeLock ? 'timelock' : 'cardlock';
const targetUrl = (isTimeLock ? '/activetimelock.html' : '/activelock.html')
+ '?lockId=' + lockId + (keyholderPending ? '&keyholderPending=1' : '');
document.getElementById('unlockCodeDisplay').textContent = code;
document.getElementById('unlockModalTitle').textContent = 'Dein Startcode';
document.getElementById('unlockModalHint').textContent =
"Öffne das TTLock mit dem Code, lege den Schlüssel in das TTLock und verschließe es anschließend wieder. Der Code verliert anschließend seine Gültigkeit";
if (keyholderPending) document.getElementById('unlockKeyholderHint').style.display = '';
const btn = document.getElementById('unlockModalBtn');
btn.textContent = "🔒 Los geht's";
btn.onclick = async () => {
btn.disabled = true;
btn.textContent = '⏳ Neuer PIN wird gesetzt…';
try {
await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' });
} catch { /* Fehler ignorieren Weiterleitung trotzdem */ }
window.location.href = targetUrl;
};
document.getElementById('unlockModal').classList.add('open');
}
// ── Entsperrcode-Modal ──
function showUnlockCodeModal(code, lockId, keyholderPending) {
document.getElementById('unlockCodeDisplay').textContent = code;