diff --git a/IMPLEMENTATION_EXACT.md b/IMPLEMENTATION_EXACT.md index 1a08c50..0ab8801 100644 --- a/IMPLEMENTATION_EXACT.md +++ b/IMPLEMENTATION_EXACT.md @@ -91,6 +91,8 @@ RETURNING *; **Verification**: Run `go build ./...` after adding this query to ensure sqlc generates the new function correctly. +**Database Schema Note**: This query uses existing fields (`devices.id`, `devices.auth_token`, `devices.updated_at`). **No schema change required** - the database structure remains the same. This is purely a new SQL query for existing schema. + --- ### 1.2 Middleware Enhancement @@ -103,7 +105,7 @@ RETURNING *; **Required Change**: Add fallback to check URL path parameter and query parameter -**REPLACE LINES 37-62** with: +**REPLACE LINES 37-270** with: ```go func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { @@ -123,8 +125,7 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF token := strings.TrimPrefix(authHeader, "Bearer ") device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), token) if err == nil { - // Found device via Bearer token, continue to validation - goto validateDevice + return m.validateDevice(c, device) } } @@ -134,8 +135,7 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF if urlToken != "" { device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), urlToken) if err == nil { - // Found device via URL path token, continue to validation - goto validateDevice + return m.validateDevice(c, device) } } @@ -145,8 +145,7 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF if queryToken != "" { device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), queryToken) if err == nil { - // Found device via query parameter token, continue to validation - goto validateDevice + return m.validateDevice(c, device) } } @@ -154,123 +153,68 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "authentication required - use Bearer token or API key", }) + } +} - validateDevice: -``` - -**Explanation**: -- `goto validateDevice` jumps to common validation code (label at line 221 in the complete function below) -- This is Go's idiomatic use of goto for error handling (allowed by PROJECT_GUIDELINES.md) -- Tries Bearer header first (for KOReader, API clients), then URL path (Kobo), then query param (OPDS) -- Each device type uses only one method: Kobo→URL path, KOReader→Bearer header -- The `validateDevice:` label is preserved in the unchanged section of the function (lines 221-270) - -**KEEP THE REST OF THE FUNCTION THE SAME** (lines 62-270 remain unchanged, including the `validateDevice:` label at line 221) - -**Complete Function After Changes** (reference for verification, shows how `goto validateDevice` connects to the label): - -```go -func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - var device database.Devices - var err error - - // Method 1: Try Bearer token header (KOReader, API clients, OPDS) - authHeader := c.Request().Header.Get("Authorization") - if authHeader != "" { - if !strings.HasPrefix(authHeader, "Bearer ") { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "invalid authorization header format", - }) - } - - token := strings.TrimPrefix(authHeader, "Bearer ") - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), token) - if err == nil { - // Found device via Bearer token, continue to validation - goto validateDevice - } - } - - // Method 2: Try URL path parameter (Kobo sync, OPDS) - // Route format: /api/sync/kobo/:token/... - urlToken := c.Param("token") - if urlToken != "" { - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), urlToken) - if err == nil { - // Found device via URL path token, continue to validation - goto validateDevice - } - } - - // Method 3: Try query parameter (OPDS catalog access) - // URL format: /opds/devices/:deviceId/catalog?token=... - queryToken := c.QueryParam("token") - if queryToken != "" { - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), queryToken) - if err == nil { - // Found device via query parameter token, continue to validation - goto validateDevice - } - } - - // All authentication methods failed - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "authentication required - use Bearer token or API key", +// validateDevice performs validation checks after successful authentication +func (m *DeviceAuthMiddleware) validateDevice(c echo.Context, device database.Devices) error { + if !device.SyncEnabled.Bool || !device.SyncEnabled.Valid { + return c.JSON(http.StatusForbidden, map[string]string{ + "error": "device sync is disabled", }) + } - validateDevice: - if !device.SyncEnabled.Bool || !device.SyncEnabled.Valid { - return c.JSON(http.StatusForbidden, map[string]string{ - "error": "device sync is disabled", - }) - } + requestType := m.getRequestType(c.Request().URL.Path) + deviceUUID := uuid.UUID(device.ID.Bytes) + deviceID := deviceUUID.String() - requestType := m.getRequestType(c.Request().URL.Path) - deviceUUID := uuid.UUID(device.ID.Bytes) - deviceID := deviceUUID.String() - - config := DeviceRateLimitConfig{ - SyncRequestsPerMinute: 60, - ProgressUpdatesPerMinute: 120, - MetadataRequestsPerMinute: 30, - } - - if !m.rateLimiter.CheckRateLimit(deviceID, requestType, config) { - remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config) - c.Response().Header().Set("X-RateLimit-Limit", "60") - c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) - c.Response().Header().Set("X-RateLimit-Reset", "60") - return c.JSON(http.StatusTooManyRequests, map[string]string{ - "error": "rate limit exceeded", - "message": "Too many requests", - "remaining": strconv.Itoa(remaining), - }) - } + config := DeviceRateLimitConfig{ + SyncRequestsPerMinute: 60, + ProgressUpdatesPerMinute: 120, + MetadataRequestsPerMinute: 30, + } + if !m.rateLimiter.CheckRateLimit(deviceID, requestType, config) { remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config) c.Response().Header().Set("X-RateLimit-Limit", "60") c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) - - ctx := DeviceContext{ - ID: device.ID.Bytes, - UserID: device.UserID.Bytes, - DeviceName: device.DeviceName, - DeviceType: device.DeviceType, - DeviceIdentifier: device.DeviceIdentifier, - SyncEnabled: device.SyncEnabled.Bool && device.SyncEnabled.Valid, - AutoSync: device.AutoSync.Bool && device.AutoSync.Valid, - } - - c.Set("device", device) - c.Set("device_ctx", ctx) - c.Set("device_id", device.ID.Bytes) - - return next(c) + c.Response().Header().Set("X-RateLimit-Reset", "60") + return c.JSON(http.StatusTooManyRequests, map[string]string{ + "error": "rate limit exceeded", + "message": "Too many requests", + "remaining": strconv.Itoa(remaining), + }) } + + remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config) + c.Response().Header().Set("X-RateLimit-Limit", "60") + c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) + + ctx := DeviceContext{ + ID: device.ID.Bytes, + UserID: device.UserID.Bytes, + DeviceName: device.DeviceName, + DeviceType: device.DeviceType, + DeviceIdentifier: device.DeviceIdentifier, + SyncEnabled: device.SyncEnabled.Bool && device.SyncEnabled.Valid, + AutoSync: device.AutoSync.Bool && device.AutoSync.Valid, + } + + c.Set("device", device) + c.Set("device_ctx", ctx) + c.Set("device_id", device.ID.Bytes) + + return next(c) } ``` +**Explanation**: +- Extracted validation logic into `validateDevice()` helper function +- Each auth method calls helper on success: `return m.validateDevice(c, device)` +- No goto statements - follows procedural style with clear control flow +- Helper function is testable, reusable, and maintainable +- Matches existing codebase patterns (no goto statements found) + **Verification Steps**: 1. Run `go build ./internal/middleware` 2. Run `go test ./internal/middleware/... -v` @@ -637,6 +581,13 @@ for _, device := range devices {
DEVICE SYNC CONFIGURATION
+ + ++ ⚠️ Security Notice: This token is sensitive. Keep it secret. If compromised, regenerate immediately. +
+