Anbindung an TTLock umgesetzt
This commit is contained in:
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
372
.metadata/.log
372
.metadata/.log
@@ -311,3 +311,375 @@ Binding(CTRL+SHIFT+T,
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
|
||||
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-24 08:03:25.447
|
||||
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||
!SESSION 2026-03-24 11:25:55.699 -----------------------------------------------
|
||||
eclipse.buildId=4.39.0.20260305-0817
|
||||
java.version=21.0.6
|
||||
java.vendor=Eclipse Adoptium
|
||||
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-24 11:25:57.216
|
||||
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-24 11:26:00.128
|
||||
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||
|
||||
!ENTRY org.eclipse.ui 2 0 2026-03-24 11:26:00.273
|
||||
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-24 11:26:00.273
|
||||
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||
|
||||
!ENTRY org.eclipse.ui 2 0 2026-03-24 11:26:00.403
|
||||
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-24 11:26:00.403
|
||||
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||
|
||||
!ENTRY org.eclipse.jface 2 0 2026-03-24 11:42:02.158
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-24 11:42:02.158
|
||||
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||
Binding(CTRL+SHIFT+T,
|
||||
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||
Open a type in a Java editor,
|
||||
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
Binding(CTRL+SHIFT+T,
|
||||
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||
,
|
||||
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 11:49:55.052
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.jface 2 0 2026-03-24 11:54:46.436
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-24 11:54:46.436
|
||||
!MESSAGE A conflict occurred for CTRL+R:
|
||||
Binding(CTRL+R,
|
||||
ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line,
|
||||
Resume and break when execution reaches the current line,
|
||||
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||
WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.debug.ui.debugging,,,system)
|
||||
Binding(CTRL+R,
|
||||
ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart,
|
||||
Restart Spring Boot Application,
|
||||
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||
WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.debug.ui.console,,,system)
|
||||
|
||||
!ENTRY org.eclipse.jdt.debug.ui 4 150 2026-03-24 11:55:55.048
|
||||
!MESSAGE Internal Error
|
||||
!STACK 1
|
||||
org.eclipse.debug.core.DebugException: Invalid stack frame
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1378)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
Caused by: java.lang.IllegalStateException
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381)
|
||||
... 12 more
|
||||
!SUBENTRY 1 org.eclipse.jdt.debug 4 100 2026-03-24 11:55:55.048
|
||||
!MESSAGE Invalid stack frame
|
||||
!STACK 0
|
||||
java.lang.IllegalStateException
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.jdt.debug.ui 4 150 2026-03-24 11:59:03.920
|
||||
!MESSAGE Internal Error
|
||||
!STACK 1
|
||||
org.eclipse.debug.core.DebugException: Invalid stack frame
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1378)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
Caused by: java.lang.IllegalStateException
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381)
|
||||
... 12 more
|
||||
!SUBENTRY 1 org.eclipse.jdt.debug 4 100 2026-03-24 11:59:03.920
|
||||
!MESSAGE Invalid stack frame
|
||||
!STACK 0
|
||||
java.lang.IllegalStateException
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingStackFrame(JDIStackFrame.java:1381)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getUnderlyingThisObject(JDIStackFrame.java:1013)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.updateVariables(JDIStackFrame.java:729)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables0(JDIStackFrame.java:397)
|
||||
at org.eclipse.jdt.internal.debug.core.model.JDIStackFrame.getVariables(JDIStackFrame.java:308)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.containsVariable(JavaDebugHover.java:620)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.lambda$2(JavaDebugHover.java:639)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.findFirstFrameForVariable(JavaDebugHover.java:604)
|
||||
at org.eclipse.jdt.internal.debug.ui.JavaDebugHover.getHoverInfo2(JavaDebugHover.java:469)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 12:19:36.658
|
||||
!MESSAGE Timeout waiting for data to generate LS hover
|
||||
!STACK 0
|
||||
java.util.concurrent.TimeoutException
|
||||
at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1960)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2095)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.ui 4 0 2026-03-24 12:42:09.972
|
||||
!MESSAGE Unhandled event loop exception
|
||||
!STACK 0
|
||||
org.eclipse.swt.SWTException: Widget is disposed
|
||||
at org.eclipse.swt.SWT.error(SWT.java:4950)
|
||||
at org.eclipse.swt.SWT.error(SWT.java:4865)
|
||||
at org.eclipse.swt.SWT.error(SWT.java:4836)
|
||||
at org.eclipse.swt.widgets.Widget.error(Widget.java:598)
|
||||
at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:513)
|
||||
at org.eclipse.swt.widgets.Control.redraw(Control.java:4613)
|
||||
at org.eclipse.swt.widgets.Link.gtk3_event_after(Link.java:453)
|
||||
at org.eclipse.swt.widgets.Widget.windowProc(Widget.java:2627)
|
||||
at org.eclipse.swt.widgets.Control.windowProc(Control.java:6833)
|
||||
at org.eclipse.swt.widgets.Display.windowProc(Display.java:6152)
|
||||
at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_do_event(Native Method)
|
||||
at org.eclipse.swt.widgets.Display.eventProc(Display.java:1624)
|
||||
at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_iteration_do(Native Method)
|
||||
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4494)
|
||||
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
|
||||
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
|
||||
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
|
||||
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
|
||||
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
|
||||
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
|
||||
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
|
||||
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
|
||||
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
|
||||
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
|
||||
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
|
||||
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
|
||||
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
|
||||
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
|
||||
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
|
||||
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
|
||||
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 13:07:28.883
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 13:16:03.467
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 16:35:09.823
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 16:39:30.159
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.debug.core 4 125 2026-03-24 17:38:12.471
|
||||
!MESSAGE Error logged from Debug Core:
|
||||
!STACK 0
|
||||
java.io.IOException: Stream closed
|
||||
at java.base/java.io.BufferedInputStream.getBufIfOpen(BufferedInputStream.java:188)
|
||||
at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:343)
|
||||
at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420)
|
||||
at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:405)
|
||||
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95)
|
||||
at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235)
|
||||
at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 17:44:12.426
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:10:52.515
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:12:21.030
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:52:19.848
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 18:52:33.597
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.lsp4e 2 0 2026-03-24 20:00:50.619
|
||||
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||
!STACK 0
|
||||
java.lang.InterruptedException
|
||||
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||
|
||||
!ENTRY org.eclipse.ui 4 0 2026-03-24 21:34:43.852
|
||||
!MESSAGE Unhandled event loop exception
|
||||
!STACK 0
|
||||
java.lang.NullPointerException: Cannot invoke "org.eclipse.e4.ui.model.application.ui.MElementContainer.getChildren()" because "root" is null
|
||||
at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.findFirstPartStackToBeRendered(CleanupAddon.java:397)
|
||||
at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.transferPrimaryDataStackIfRemoved(CleanupAddon.java:390)
|
||||
at org.eclipse.e4.ui.workbench.addons.cleanupaddon.CleanupAddon.lambda$1(CleanupAddon.java:356)
|
||||
at org.eclipse.swt.widgets.RunnableLock.run(RunnableLock.java:40)
|
||||
at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:132)
|
||||
at org.eclipse.swt.widgets.Display.runAsyncMessages(Display.java:5035)
|
||||
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4500)
|
||||
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
|
||||
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
|
||||
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
|
||||
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
|
||||
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
|
||||
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
|
||||
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
|
||||
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
|
||||
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
|
||||
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
|
||||
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
|
||||
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
|
||||
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
|
||||
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
|
||||
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
|
||||
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
|
||||
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
bilder/toys/doppelpnsknebel.png
Normal file
BIN
bilder/toys/doppelpnsknebel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
bilder/toys/raw/41-54Dp0rLL._AC_SY450_.jpg
Normal file
BIN
bilder/toys/raw/41-54Dp0rLL._AC_SY450_.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -37,6 +37,7 @@ public class KeyholderNotificationEntity {
|
||||
@Column(nullable = false)
|
||||
private boolean notifiedKeyholder = false;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private TempOpeningReason openingReason;
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public abstract class LockControl {
|
||||
public abstract boolean unlock();
|
||||
|
||||
public abstract boolean lock();
|
||||
|
||||
public abstract boolean cleanup();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,5 +19,10 @@ public class TrustLockControl extends LockControl {
|
||||
@Override
|
||||
public boolean lock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cleanup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,9 @@ public class UnlockcodeLockControl extends LockControl {
|
||||
callback.setUnlockCode(code);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cleanup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
package de.oaa.xxx.games.chastity.unlock;
|
||||
|
||||
public enum TempOpeningReason {
|
||||
HYGIENE, CARD, TASK;
|
||||
HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
async function ttlockOpenOnce() {
|
||||
const btn = document.getElementById('ttl-open-btn');
|
||||
const errEl = document.getElementById('ttl-open-error');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ …';
|
||||
errEl.textContent = '';
|
||||
|
||||
try {
|
||||
const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
if (!r.ok) {
|
||||
const msgs = {
|
||||
ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.',
|
||||
admin_config_missing: 'Systemkonfiguration fehlt (Admin).',
|
||||
auth_failed: 'Anmeldung bei TTLock fehlgeschlagen.',
|
||||
passcode_failed: 'PIN konnte nicht am Schloss angelegt werden.',
|
||||
active_lock_exists: 'Du befindest dich in einem aktiven Lock – manuelles Öffnen ist nicht möglich.',
|
||||
};
|
||||
errEl.textContent = msgs[data.error] || 'Unbekannter Fehler.';
|
||||
return;
|
||||
}
|
||||
|
||||
const pwdId = data.keyboardPwdId;
|
||||
document.getElementById('ttl-open-pin').textContent = data.pin;
|
||||
|
||||
const modal = document.getElementById('ttlOpenModal');
|
||||
modal.classList.add('visible');
|
||||
|
||||
const okBtn = document.getElementById('ttl-open-ok-btn');
|
||||
const onOk = async () => {
|
||||
okBtn.removeEventListener('click', onOk);
|
||||
okBtn.disabled = true;
|
||||
modal.classList.remove('visible');
|
||||
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
|
||||
okBtn.disabled = false;
|
||||
};
|
||||
okBtn.addEventListener('click', onOk);
|
||||
} catch {
|
||||
errEl.textContent = 'Netzwerkfehler.';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔓 Öffnen';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTtlockUserConfig() {
|
||||
const errEl = document.getElementById('ttl-error');
|
||||
errEl.textContent = '';
|
||||
const lockIdVal = document.getElementById('ttl-lockid').value.trim();
|
||||
const body = {
|
||||
username: document.getElementById('ttl-username').value.trim(),
|
||||
password: document.getElementById('ttl-password').value,
|
||||
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
|
||||
};
|
||||
const r = await fetch('/user/me/ttlock', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (r.ok) {
|
||||
showToast();
|
||||
await loadTtlockUserConfig();
|
||||
} else {
|
||||
errEl.textContent = 'Fehler beim Speichern.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 (4–9 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;
|
||||
|
||||
Reference in New Issue
Block a user