From 8e4412544d777a457a3bbd7935db3ebd87b98695 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 2 Jun 2026 19:45:14 -0400 Subject: [PATCH] fix(devices): move device creation from CheckRegistration to ApproveDevice Previously device creation happened in CheckRegistrationStatus (polling endpoint), which was racy. Now the admin's ApproveDevice handler creates the device record and stores auth token + device ID on the registration entry. CheckRegistrationStatus just returns the pre-created credentials. Also adds approved/authToken/deviceID/syncEndpoints fields to PendingRegistration struct. --- internal/handlers/devices.go | 124 +++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/internal/handlers/devices.go b/internal/handlers/devices.go index b736518..f62d45e 100644 --- a/internal/handlers/devices.go +++ b/internal/handlers/devices.go @@ -100,6 +100,10 @@ type PendingRegistration struct { UserID uuid.UUID ExpiresAt time.Time CreatedAt time.Time + Approved bool + AuthToken string + DeviceID [16]byte + SyncEndpoints map[string]string } var pendingRegistrations = make(map[string]*PendingRegistration) @@ -173,60 +177,21 @@ func (h *DeviceHandler) CheckRegistrationStatus(c *echo.Context) error { return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"}) } - if registration.UserID == (uuid.UUID{}) { + if registration.Approved { + delete(pendingRegistrations, req.RegistrationID) + return c.JSON(http.StatusOK, DeviceAuthStatusResponse{ - Status: "pending", - Message: "awaiting user approval", - ExpiresIn: int(time.Until(registration.ExpiresAt).Seconds()), + Status: "approved", + AuthToken: registration.AuthToken, + DeviceID: registration.DeviceID, + SyncEndpoints: registration.SyncEndpoints, }) } - authToken, err := generateDeviceToken() - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate auth token"}) - } - - userUUID := registration.UserID - pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true} - - syncEnabled := pgtype.Bool{Bool: true, Valid: true} - autoSync := pgtype.Bool{Bool: true, Valid: true} - syncFreq := pgtype.Int4{Int32: 5, Valid: true} - - device, err := h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{ - UserID: pgUserID, - DeviceName: registration.DeviceName, - DeviceType: registration.DeviceType, - DeviceIdentifier: registration.DeviceIdentifier, - AuthToken: authToken, - SyncEnabled: syncEnabled, - AutoSync: autoSync, - SyncFrequencyMinutes: syncFreq, - DeviceMetadata: []byte("{}"), - }) - - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create device"}) - } - - delete(pendingRegistrations, req.RegistrationID) - - syncEndpoints := map[string]string{} - switch registration.DeviceType { - case "koreader": - syncEndpoints["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", h.cfg.BaseURL) - syncEndpoints["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", h.cfg.BaseURL) - syncEndpoints["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", h.cfg.BaseURL) - case "kobo": - syncEndpoints["markup"] = fmt.Sprintf("%s/api/sync/kobo/markup", h.cfg.BaseURL) - syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", h.cfg.BaseURL) - } - return c.JSON(http.StatusOK, DeviceAuthStatusResponse{ - Status: "approved", - AuthToken: authToken, - DeviceID: device.ID.Bytes, - SyncEndpoints: syncEndpoints, + Status: "pending", + Message: "awaiting user approval", + ExpiresIn: int(time.Until(registration.ExpiresAt).Seconds()), }) } @@ -597,14 +562,63 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error { return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"}) } + if registration.Approved { + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "device already approved", + "device_name": registration.DeviceName, + "device_type": registration.DeviceType, + "approved": true, + }) + } + + authToken, err := generateDeviceToken() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate auth token"}) + } + + pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true} + syncEnabled := pgtype.Bool{Bool: true, Valid: true} + autoSync := pgtype.Bool{Bool: true, Valid: true} + syncFreq := pgtype.Int4{Int32: 5, Valid: true} + + device, err := h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{ + UserID: pgUserID, + DeviceName: registration.DeviceName, + DeviceType: registration.DeviceType, + DeviceIdentifier: registration.DeviceIdentifier, + AuthToken: authToken, + SyncEnabled: syncEnabled, + AutoSync: autoSync, + SyncFrequencyMinutes: syncFreq, + DeviceMetadata: []byte("{}"), + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create device"}) + } + + syncEndpoints := map[string]string{} + switch registration.DeviceType { + case "koreader": + syncEndpoints["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", h.cfg.BaseURL) + syncEndpoints["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", h.cfg.BaseURL) + syncEndpoints["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", h.cfg.BaseURL) + case "kobo": + syncEndpoints["markup"] = fmt.Sprintf("%s/api/sync/kobo/markup", h.cfg.BaseURL) + syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", h.cfg.BaseURL) + } + registration.UserID = userUUID + registration.Approved = true + registration.AuthToken = authToken + registration.DeviceID = device.ID.Bytes + registration.SyncEndpoints = syncEndpoints return c.JSON(http.StatusOK, map[string]interface{}{ "message": "device approved successfully", "device_name": registration.DeviceName, "device_type": registration.DeviceType, "registration_id": registrationID, - "approved": true, // Fixed: Add confirmation field for test compatibility + "approved": true, }) } @@ -624,15 +638,9 @@ func (h *DeviceHandler) RejectDevice(c *echo.Context) error { } func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[string]interface{}, error) { - userID := c.Get("user_id").(string) - userUUID, err := uuid.Parse(userID) - if err != nil { - return nil, err - } - registrations := []map[string]interface{}{} for _, reg := range pendingRegistrations { - if reg.UserID == userUUID || reg.UserID == (uuid.UUID{}) { + if reg.UserID == (uuid.UUID{}) { registrations = append(registrations, map[string]interface{}{ "registration_id": reg.RegistrationID, "device_name": reg.DeviceName, @@ -640,7 +648,7 @@ func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[stri "device_identifier": reg.DeviceIdentifier, "expires_at": reg.ExpiresAt, "created_at": reg.CreatedAt, - "is_approved": reg.UserID != (uuid.UUID{}), + "is_approved": false, }) } }