diff --git a/IMPLEMENTATION_EXACT.md b/IMPLEMENTATION_EXACT.md
index 0ab8801..fe1fba8 100644
--- a/IMPLEMENTATION_EXACT.md
+++ b/IMPLEMENTATION_EXACT.md
@@ -11,15 +11,12 @@
## Table of Contents
1. [Phase 1: Enhanced Authentication (Week 1)](#phase-1-enhanced-authentication)
- - Database Query Addition
- - Middleware Enhancement
- - Router Updates
- - Backend Handler Addition
- - Frontend Template Updates
- - Bruno API Tests
+ - Database Query Addition
+ - Middleware Enhancement
+ - Router Updates
+ - Backend Handler Addition
+ - Frontend Template Updates
2. [Phase 2: Kobo Integration (Week 1-2)](#phase-2-kobo-integration)
- - Documentation Updates
- - Test Updates
3. [Phase 3: OPDS Security (Week 2-3)](#phase-3-opds-security)
- Router Enhancement
- Bruno API Tests
@@ -281,7 +278,7 @@ koboSync.POST("/sync-from-server", cfg.DeviceAuthMiddleware.Authenticate(koboHan
**File**: `internal/router/device.go`
-**Location**: After device registration routes (around line 50)
+**Location**: After device registration routes (around line 25)
**Current Implementation**: Need to check what device routes exist
@@ -294,23 +291,16 @@ koboSync.POST("/sync-from-server", cfg.DeviceAuthMiddleware.Authenticate(koboHan
devices.PUT("/:id/regenerate-token", jwtMiddleware, h.RegenerateDeviceToken)
```
-**Complete Context** (assuming placement after device registration routes):
-
+ **Complete Context** (token regeneration route added to existing device routes):
```go
-// Device registration endpoints
-devices.POST("/register", h.InitiateRegistration)
-devices.POST("/approve/:registration_id", jwtMiddleware, h.ApproveDevice)
-devices.POST("/reject/:registration_id", jwtMiddleware, h.RejectDevice)
-devices.GET("/pending", jwtMiddleware, h.ListPendingRegistrations)
+// Existing device management routes (unchanged)
+devices.GET("", jwtMiddleware, cfg.DeviceHandler.ListDevices)
+devices.GET("/:id", jwtMiddleware, cfg.DeviceHandler.GetDevice)
+devices.PUT("/:id", jwtMiddleware, cfg.DeviceHandler.UpdateDevice)
+devices.DELETE("/:id", jwtMiddleware, cfg.DeviceHandler.DeleteDevice)
-// Device management endpoints
-devices.GET("", jwtMiddleware, h.ListDevices)
-devices.GET("/:id", jwtMiddleware, h.GetDevice)
-devices.PUT("/:id", jwtMiddleware, h.UpdateDevice)
-devices.DELETE("/:id", jwtMiddleware, h.DeleteDevice)
-
-// Token regeneration endpoint (JWT authentication required)
-devices.PUT("/:id/regenerate-token", jwtMiddleware, h.RegenerateDeviceToken)
+// NEW: Token regeneration endpoint (JWT authentication required)
+devices.PUT("/:id/regenerate-token", jwtMiddleware, cfg.DeviceHandler.RegenerateDeviceToken)
```
**Verification**: Run `go build ./internal/router`
@@ -517,13 +507,16 @@ deviceList[i] = DeviceInfo{
**File**: `templates/devices.templ`
-**Location**: Lines 46-98 (device card in grid)
+**Location**: Template function signature (line 5) and device card (lines 46-98)
-**Current Implementation**: Device card shows device info and settings buttons
+**Required Changes**:
-**Required Addition**: Add buttons for copy sync URL and regenerate token
+1. **Update template signature** to accept baseURL parameter (line 5):
+```templ
+templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, baseURL string) {
+```
-**REPLACE DEVICE CARD CONTENT** (lines 46-98) with:
+2. **REPLACE DEVICE CARD CONTENT** (lines 46-98) with:
```templ
for _, device := range devices {
@@ -598,12 +591,12 @@ for _, device := range devices {
type="text"
id="sync-url-{ device.ID }"
readonly
- value="{ fmt.Sprintf("http://YOUR_IP:8765/api/sync/kobo/%s", device.AuthToken) }"
+ value="{ fmt.Sprintf("%s/api/sync/kobo/%s", baseURL, device.AuthToken) }"
class="flex-1 px-3 py-2 text-xs rounded border"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/>
Sync Status ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Sync Status ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -137,7 +137,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSync.Format("2006-01-02 15:04"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 85, Col: 97}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 86, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -165,7 +165,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSeen.Format("2006-01-02 15:04"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 93, Col: 97}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 94, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -181,76 +181,118 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
DEVICE SYNC CONFIGURATION
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if device.DeviceType == "kobo" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if device.DeviceType == "koreader" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
🔄 Regenerate Token⚠️ Old token will immediately stop working
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(pendingRegistrations) > 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Pending Device Registrations
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
Pending Device Registrations
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, reg := range pendingRegistrations {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var6 string
- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 112, Col: 87}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var7 string
- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 27}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " - Expires in ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 58}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 178, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
Approve Reject
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 180, Col: 27}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " - Expires in ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 180, Col: 58}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
Approve Reject
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/device-management.ts b/web/src/device-management.ts
new file mode 100644
index 0000000..0599c1c
--- /dev/null
+++ b/web/src/device-management.ts
@@ -0,0 +1,97 @@
+// Device Management - Token copy and regeneration
+// Procedural style with proper types (no OOP)
+
+interface RegenerateTokenResponse {
+ message: string;
+ auth_token: string;
+ device: {
+ id: string;
+ device_name: string;
+ device_type: string;
+ auth_token: string;
+ sync_enabled: boolean;
+ auto_sync: boolean;
+ sync_frequency_minutes: number;
+ };
+ sync_urls?: {
+ sync_url?: string;
+ markup?: string;
+ bookmark?: string;
+ init?: string;
+ progress?: string;
+ metadata?: string;
+ bookmarks?: string;
+ };
+}
+
+// Copy sync URL or auth token to clipboard
+function copyToClipboard(text: string, label: string): void {
+ navigator.clipboard.writeText(text)
+ .then(() => {
+ const toast = (window as any).showToast;
+ if (toast) {
+ toast.success(`${label} copied to clipboard`);
+ }
+ })
+ .catch((err: unknown) => {
+ console.error('Failed to copy:', err);
+ const toast = (window as any).showToast;
+ if (toast) {
+ toast.error('Failed to copy to clipboard');
+ }
+ });
+}
+
+// Regenerate device token with confirmation
+function regenerateDeviceToken(deviceId: string, event: Event): void {
+ const confirmation = '⚠️ This will revoke current token and generate a new one.\n\n' +
+ 'The old token will immediately stop working.\n\n' +
+ 'You will need to update your device configuration with new token.\n\n' +
+ 'Continue?';
+
+ if (!confirm(confirmation)) {
+ return;
+ }
+
+ const btn = event.target as HTMLButtonElement;
+ const originalText = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = '🔄 Regenerating...';
+
+ fetch(`/api/devices/${deviceId}/regenerate-token`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ })
+ .then((response: Response) => {
+ if (!response.ok) {
+ throw new Error('Failed to regenerate token');
+ }
+ return response.json() as Promise;
+ })
+ .then((_data: RegenerateTokenResponse) => {
+ const toast = (window as any).showToast;
+ if (toast) {
+ toast.success('Token regenerated successfully - update your device config');
+ }
+ // Reload page to show new token
+ setTimeout(() => location.reload(), 1500);
+ })
+ .catch((error: unknown) => {
+ console.error('Error:', error);
+ const toast = (window as any).showToast;
+ if (toast) {
+ toast.error('Failed to regenerate token');
+ }
+ if (btn) {
+ btn.disabled = false;
+ btn.innerHTML = originalText;
+ }
+ });
+}
+
+// Export functions for global access (called from template onclick attributes)
+window.copyToClipboard = copyToClipboard;
+window.regenerateDeviceToken = regenerateDeviceToken;
+