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.
This commit is contained in:
2026-06-02 19:45:14 -04:00
parent d764f820b2
commit 8e4412544d
+66 -58
View File
@@ -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,
})
}
}