Update all handler functions to use *echo.Context (pointer) instead of echo.Context (value) as required by Echo v5. Changes across all handler files: - analytics.go: Update handler signatures - auth.go: Update authentication handler signatures - book_matching.go: Update matching handler signatures - collections.go: Update collection handler signatures - collections_preview_test.go: Update test signatures - commonhandlers.go: Update common handler signatures - conflicts.go: Update conflict handler signatures - context.go: Update context handler signatures - dashboard.go: Update dashboard handler signatures - devices.go: Update device handler signatures - jobs.go: Update job handler signatures - kobo.go: Update Kobo handler signatures - koreader.go: Update Koreader handler signatures - library.go: Update library handler signatures - matching.go: Update matching handler signatures - media.go: Update media handler signatures - opds.go: Update OPDS handler signatures - progress.go: Update progress handler signatures - queue.go: Update queue handler signatures - refresh_token.go: Update token handler signatures - scanner.go: Update scanner handler signatures - sidecar.go: Update sidecar handler signatures - sync.go: Update sync handler signatures - system_settings.go: Update settings handler signatures - websocket.go: Update WebSocket handler signatures All handlers now properly implement Echo v5's pointer-based context pattern. This change is necessary for type safety and compatibility with Echo v5's improved context handling and WebSocket support.
695 lines
22 KiB
Go
695 lines
22 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/config"
|
|
"bookhoard/internal/database"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
"github.com/skip2/go-qrcode"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type DeviceHandler struct {
|
|
db *database.Queries
|
|
jwtKey []byte
|
|
cfg *config.Config
|
|
}
|
|
|
|
func NewDeviceHandler(db *database.Queries, jwtSecret string, cfg *config.Config) *DeviceHandler {
|
|
return &DeviceHandler{
|
|
db: db,
|
|
jwtKey: []byte(jwtSecret),
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
type DeviceRegistrationRequest struct {
|
|
DeviceName string `json:"device_name" validate:"required,min=1,max=100"`
|
|
DeviceType string `json:"device_type" validate:"required,oneof=koreader kobo web mobile"`
|
|
DeviceIdentifier string `json:"device_identifier" validate:"required,min=1,max=255"`
|
|
}
|
|
|
|
type DeviceRegistrationResponse struct {
|
|
DeviceID uuid.UUID `json:"device_id"`
|
|
RegistrationID string `json:"registration_id"`
|
|
AuthURL string `json:"auth_url"`
|
|
QRCode string `json:"qr_code"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
PollInterval int `json:"poll_interval"`
|
|
SetupInstructions map[string]string `json:"setup_instructions"`
|
|
}
|
|
|
|
type DeviceAuthStatusRequest struct {
|
|
RegistrationID string `json:"registration_id" validate:"required"`
|
|
}
|
|
|
|
type DeviceAuthStatusResponse struct {
|
|
Status string `json:"status"`
|
|
AuthToken string `json:"auth_token,omitempty"`
|
|
DeviceID uuid.UUID `json:"device_id,omitempty"`
|
|
SyncEndpoints map[string]string `json:"sync_endpoints,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
ExpiresIn int `json:"expires_in,omitempty"`
|
|
}
|
|
|
|
type DeviceListResponse struct {
|
|
Devices []DeviceInfo `json:"devices"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type DeviceInfo struct {
|
|
ID uuid.UUID `json:"id"`
|
|
DeviceName string `json:"device_name"`
|
|
DeviceType string `json:"device_type"`
|
|
LastSync *time.Time `json:"last_sync"`
|
|
LastSeen *time.Time `json:"last_seen"`
|
|
SyncEnabled bool `json:"sync_enabled"`
|
|
AutoSync bool `json:"auto_sync"`
|
|
SyncFrequency int32 `json:"sync_frequency_minutes"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
DeviceMetadata json.RawMessage `json:"device_metadata,omitempty"`
|
|
AuthToken string `json:"auth_token"`
|
|
}
|
|
|
|
type DeviceUpdateRequest struct {
|
|
DeviceName string `json:"device_name,omitempty" validate:"omitempty,min=1,max=100"`
|
|
SyncEnabled *bool `json:"sync_enabled,omitempty"`
|
|
AutoSync *bool `json:"auto_sync,omitempty"`
|
|
SyncFrequencyMinutes *int32 `json:"sync_frequency_minutes,omitempty" validate:"omitempty,min=1,max=1440"`
|
|
DeviceMetadata json.RawMessage `json:"device_metadata,omitempty"`
|
|
}
|
|
|
|
type DeviceApprovalRequest struct {
|
|
RegistrationID string `json:"registration_id" validate:"required"`
|
|
Approve bool `json:"approve"`
|
|
}
|
|
|
|
type PendingRegistration struct {
|
|
RegistrationID string
|
|
DeviceName string
|
|
DeviceType string
|
|
DeviceIdentifier string
|
|
UserID uuid.UUID
|
|
ExpiresAt time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
var pendingRegistrations = make(map[string]*PendingRegistration)
|
|
|
|
func (h *DeviceHandler) InitiateRegistration(c *echo.Context) error {
|
|
req := DeviceRegistrationRequest{}
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
|
|
}
|
|
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
registrationID := uuid.New().String()
|
|
expiresAt := time.Now().Add(5 * time.Minute)
|
|
|
|
registration := &PendingRegistration{
|
|
RegistrationID: registrationID,
|
|
DeviceName: req.DeviceName,
|
|
DeviceType: req.DeviceType,
|
|
DeviceIdentifier: req.DeviceIdentifier,
|
|
ExpiresAt: expiresAt,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
pendingRegistrations[registrationID] = registration
|
|
|
|
authURL := fmt.Sprintf("%s/devices/approve/%s", h.cfg.BaseURL, registrationID)
|
|
|
|
qrCode, err := qrcode.Encode(authURL, qrcode.Medium, 256)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate QR code"})
|
|
}
|
|
|
|
qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)
|
|
|
|
setupInstructions := map[string]string{}
|
|
switch req.DeviceType {
|
|
case "koreader":
|
|
setupInstructions["koreader"] = fmt.Sprintf("Calibre URL: %s/api/sync/koreader", h.cfg.BaseURL)
|
|
case "kobo":
|
|
setupInstructions["kobo"] = fmt.Sprintf("Sync URL: %s/api/sync/kobo", h.cfg.BaseURL)
|
|
}
|
|
|
|
response := DeviceRegistrationResponse{
|
|
RegistrationID: registrationID,
|
|
AuthURL: authURL,
|
|
QRCode: "data:image/png;base64," + qrCodeBase64,
|
|
ExpiresIn: 300,
|
|
PollInterval: 3,
|
|
SetupInstructions: setupInstructions,
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, response)
|
|
}
|
|
|
|
func (h *DeviceHandler) CheckRegistrationStatus(c *echo.Context) error {
|
|
req := DeviceAuthStatusRequest{}
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
|
|
}
|
|
|
|
registration, exists := pendingRegistrations[req.RegistrationID]
|
|
if !exists {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
|
}
|
|
|
|
if time.Now().After(registration.ExpiresAt) {
|
|
delete(pendingRegistrations, req.RegistrationID)
|
|
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
|
|
}
|
|
|
|
if registration.UserID == (uuid.UUID{}) {
|
|
return c.JSON(http.StatusOK, DeviceAuthStatusResponse{
|
|
Status: "pending",
|
|
Message: "awaiting user approval",
|
|
ExpiresIn: int(time.Until(registration.ExpiresAt).Seconds()),
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) ListDevices(c *echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true}
|
|
|
|
devices, err := h.db.ListDevicesByUser(c.Request().Context(), pgUserID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list devices"})
|
|
}
|
|
|
|
deviceList := make([]DeviceInfo, 0, len(devices))
|
|
for _, device := range devices {
|
|
syncEnabled := device.SyncEnabled.Bool && device.SyncEnabled.Valid
|
|
autoSync := device.AutoSync.Bool && device.AutoSync.Valid
|
|
syncFreq := int32(0)
|
|
if device.SyncFrequencyMinutes.Valid {
|
|
syncFreq = device.SyncFrequencyMinutes.Int32
|
|
}
|
|
|
|
deviceList = append(deviceList, DeviceInfo{
|
|
ID: device.ID.Bytes,
|
|
DeviceName: device.DeviceName,
|
|
DeviceType: device.DeviceType,
|
|
LastSync: (*time.Time)(&device.LastSync.Time),
|
|
LastSeen: (*time.Time)(&device.LastSeen.Time),
|
|
SyncEnabled: syncEnabled,
|
|
AutoSync: autoSync,
|
|
SyncFrequency: syncFreq,
|
|
CreatedAt: device.CreatedAt.Time,
|
|
DeviceMetadata: device.DeviceMetadata,
|
|
AuthToken: device.AuthToken,
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, DeviceListResponse{
|
|
Devices: deviceList,
|
|
Total: len(deviceList),
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) GetDevicesData(c *echo.Context) ([]DeviceInfo, error) {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true}
|
|
|
|
devices, err := h.db.ListDevicesByUser(c.Request().Context(), pgUserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deviceList := make([]DeviceInfo, 0, len(devices))
|
|
for _, device := range devices {
|
|
syncEnabled := device.SyncEnabled.Bool && device.SyncEnabled.Valid
|
|
autoSync := device.AutoSync.Bool && device.AutoSync.Valid
|
|
syncFreq := int32(0)
|
|
if device.SyncFrequencyMinutes.Valid {
|
|
syncFreq = device.SyncFrequencyMinutes.Int32
|
|
}
|
|
|
|
deviceList = append(deviceList, DeviceInfo{
|
|
ID: device.ID.Bytes,
|
|
DeviceName: device.DeviceName,
|
|
DeviceType: device.DeviceType,
|
|
LastSync: (*time.Time)(&device.LastSync.Time),
|
|
LastSeen: (*time.Time)(&device.LastSeen.Time),
|
|
SyncEnabled: syncEnabled,
|
|
AutoSync: autoSync,
|
|
SyncFrequency: syncFreq,
|
|
CreatedAt: device.CreatedAt.Time,
|
|
DeviceMetadata: device.DeviceMetadata,
|
|
AuthToken: device.AuthToken,
|
|
})
|
|
}
|
|
|
|
return deviceList, nil
|
|
}
|
|
|
|
func (h *DeviceHandler) GetDevice(c *echo.Context) error {
|
|
userID := c.Get("user_id")
|
|
if userID == nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID.(string))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
|
}
|
|
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
|
|
device, err := h.db.GetDevice(c.Request().Context(), pgDeviceID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
|
|
}
|
|
|
|
if device.UserID.Bytes != userUUID {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"})
|
|
}
|
|
|
|
syncEnabled := device.SyncEnabled.Bool && device.SyncEnabled.Valid
|
|
autoSync := device.AutoSync.Bool && device.AutoSync.Valid
|
|
syncFreq := int32(0)
|
|
if device.SyncFrequencyMinutes.Valid {
|
|
syncFreq = device.SyncFrequencyMinutes.Int32
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, DeviceInfo{
|
|
ID: device.ID.Bytes,
|
|
DeviceName: device.DeviceName,
|
|
DeviceType: device.DeviceType,
|
|
LastSync: (*time.Time)(&device.LastSync.Time),
|
|
LastSeen: (*time.Time)(&device.LastSeen.Time),
|
|
SyncEnabled: syncEnabled,
|
|
AutoSync: autoSync,
|
|
SyncFrequency: syncFreq,
|
|
CreatedAt: device.CreatedAt.Time,
|
|
DeviceMetadata: device.DeviceMetadata,
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) UpdateDevice(c *echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
|
}
|
|
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
|
|
device, err := h.db.GetDevice(c.Request().Context(), pgDeviceID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
|
|
}
|
|
|
|
if device.UserID.Bytes != userUUID {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"})
|
|
}
|
|
|
|
req := DeviceUpdateRequest{}
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
|
|
}
|
|
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
updateParams := database.UpdateDeviceParams{
|
|
ID: pgDeviceID,
|
|
}
|
|
|
|
if req.DeviceName != "" {
|
|
updateParams.DeviceName = req.DeviceName
|
|
} else {
|
|
updateParams.DeviceName = device.DeviceName
|
|
}
|
|
|
|
if req.SyncEnabled != nil {
|
|
updateParams.SyncEnabled = pgtype.Bool{Bool: *req.SyncEnabled, Valid: true}
|
|
} else {
|
|
updateParams.SyncEnabled = device.SyncEnabled
|
|
}
|
|
|
|
if req.AutoSync != nil {
|
|
updateParams.AutoSync = pgtype.Bool{Bool: *req.AutoSync, Valid: true}
|
|
} else {
|
|
updateParams.AutoSync = device.AutoSync
|
|
}
|
|
|
|
if req.SyncFrequencyMinutes != nil {
|
|
updateParams.SyncFrequencyMinutes = pgtype.Int4{Int32: *req.SyncFrequencyMinutes, Valid: true}
|
|
} else {
|
|
updateParams.SyncFrequencyMinutes = device.SyncFrequencyMinutes
|
|
}
|
|
|
|
if req.DeviceMetadata != nil {
|
|
updateParams.DeviceMetadata = req.DeviceMetadata
|
|
} else {
|
|
updateParams.DeviceMetadata = device.DeviceMetadata
|
|
}
|
|
|
|
updatedDevice, err := h.db.UpdateDevice(c.Request().Context(), updateParams)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update device"})
|
|
}
|
|
|
|
syncEnabled := updatedDevice.SyncEnabled.Bool && updatedDevice.SyncEnabled.Valid
|
|
autoSync := updatedDevice.AutoSync.Bool && updatedDevice.AutoSync.Valid
|
|
syncFreq := int32(0)
|
|
if updatedDevice.SyncFrequencyMinutes.Valid {
|
|
syncFreq = updatedDevice.SyncFrequencyMinutes.Int32 // Fixed: Use actual updated value
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"device_updated": true,
|
|
"device": DeviceInfo{
|
|
ID: updatedDevice.ID.Bytes,
|
|
DeviceName: updatedDevice.DeviceName,
|
|
DeviceType: updatedDevice.DeviceType,
|
|
LastSync: (*time.Time)(&updatedDevice.LastSync.Time),
|
|
LastSeen: (*time.Time)(&updatedDevice.LastSeen.Time),
|
|
SyncEnabled: syncEnabled,
|
|
AutoSync: autoSync,
|
|
SyncFrequency: syncFreq, // Now correctly returns the updated value
|
|
CreatedAt: updatedDevice.CreatedAt.Time,
|
|
DeviceMetadata: updatedDevice.DeviceMetadata,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) DeleteDevice(c *echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
|
}
|
|
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
|
|
device, err := h.db.GetDevice(c.Request().Context(), pgDeviceID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
|
|
}
|
|
|
|
if device.UserID.Bytes != userUUID {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"})
|
|
}
|
|
|
|
if err := h.db.DeleteDevice(c.Request().Context(), pgDeviceID); err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to delete device"})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *DeviceHandler) RegenerateDeviceToken(c *echo.Context) error {
|
|
// Verify JWT authentication
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
// Parse device ID from URL parameter
|
|
deviceID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
|
}
|
|
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
|
|
// Verify device exists and belongs to user
|
|
device, err := h.db.GetDevice(c.Request().Context(), pgDeviceID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
|
|
}
|
|
|
|
if device.UserID.Bytes != userUUID {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"})
|
|
}
|
|
|
|
// Generate new auth token
|
|
newToken, err := generateDeviceToken()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
|
}
|
|
|
|
// Update device with new token
|
|
updatedDevice, err := h.db.UpdateDeviceAuthToken(c.Request().Context(), database.UpdateDeviceAuthTokenParams{
|
|
ID: pgDeviceID,
|
|
AuthToken: newToken,
|
|
})
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update token"})
|
|
}
|
|
|
|
// Return new token with device info
|
|
syncEnabled := updatedDevice.SyncEnabled.Bool && updatedDevice.SyncEnabled.Valid
|
|
autoSync := updatedDevice.AutoSync.Bool && updatedDevice.AutoSync.Valid
|
|
syncFreq := int32(0)
|
|
if updatedDevice.SyncFrequencyMinutes.Valid {
|
|
syncFreq = updatedDevice.SyncFrequencyMinutes.Int32
|
|
}
|
|
|
|
// Build sync URLs with new token
|
|
syncURLs := map[string]string{}
|
|
baseURL := h.cfg.BaseURL
|
|
|
|
switch updatedDevice.DeviceType {
|
|
case "kobo":
|
|
syncURLs["sync_url"] = fmt.Sprintf("%s/api/sync/kobo/%s", baseURL, newToken)
|
|
syncURLs["markup"] = fmt.Sprintf("%s/api/sync/kobo/%s/markup", baseURL, newToken)
|
|
syncURLs["bookmark"] = fmt.Sprintf("%s/api/sync/kobo/%s/bookmark", baseURL, newToken)
|
|
syncURLs["init"] = fmt.Sprintf("%s/api/sync/kobo/%s/v1/initialization", baseURL, newToken)
|
|
case "koreader":
|
|
syncURLs["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", baseURL)
|
|
syncURLs["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", baseURL)
|
|
syncURLs["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", baseURL)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"message": "Token regenerated successfully",
|
|
"auth_token": newToken,
|
|
"device": DeviceInfo{
|
|
ID: updatedDevice.ID.Bytes,
|
|
DeviceName: updatedDevice.DeviceName,
|
|
DeviceType: updatedDevice.DeviceType,
|
|
LastSync: (*time.Time)(&updatedDevice.LastSync.Time),
|
|
LastSeen: (*time.Time)(&updatedDevice.LastSeen.Time),
|
|
SyncEnabled: syncEnabled,
|
|
AutoSync: autoSync,
|
|
SyncFrequency: syncFreq,
|
|
CreatedAt: updatedDevice.CreatedAt.Time,
|
|
DeviceMetadata: updatedDevice.DeviceMetadata,
|
|
},
|
|
"sync_urls": syncURLs,
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
registrationID := c.Param("registration_id")
|
|
|
|
registration, exists := pendingRegistrations[registrationID]
|
|
if !exists {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
|
}
|
|
|
|
if time.Now().After(registration.ExpiresAt) {
|
|
delete(pendingRegistrations, registrationID)
|
|
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
|
|
}
|
|
|
|
registration.UserID = userUUID
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
func (h *DeviceHandler) RejectDevice(c *echo.Context) error {
|
|
registrationID := c.Param("registration_id")
|
|
|
|
_, exists := pendingRegistrations[registrationID]
|
|
if !exists {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
|
}
|
|
|
|
delete(pendingRegistrations, registrationID)
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{
|
|
"message": "device registration rejected",
|
|
})
|
|
}
|
|
|
|
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{}) {
|
|
registrations = append(registrations, map[string]interface{}{
|
|
"registration_id": reg.RegistrationID,
|
|
"device_name": reg.DeviceName,
|
|
"device_type": reg.DeviceType,
|
|
"device_identifier": reg.DeviceIdentifier,
|
|
"expires_at": reg.ExpiresAt,
|
|
"created_at": reg.CreatedAt,
|
|
"is_approved": reg.UserID != (uuid.UUID{}),
|
|
})
|
|
}
|
|
}
|
|
|
|
return registrations, nil
|
|
}
|
|
|
|
func (h *DeviceHandler) ListPendingRegistrations(c *echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
|
|
}
|
|
|
|
registrations := []map[string]interface{}{}
|
|
for _, reg := range pendingRegistrations {
|
|
if reg.UserID == userUUID || reg.UserID == (uuid.UUID{}) {
|
|
registrations = append(registrations, map[string]interface{}{
|
|
"registration_id": reg.RegistrationID,
|
|
"device_name": reg.DeviceName,
|
|
"device_type": reg.DeviceType,
|
|
"device_identifier": reg.DeviceIdentifier,
|
|
"expires_at": reg.ExpiresAt,
|
|
"created_at": reg.CreatedAt,
|
|
"is_approved": reg.UserID != (uuid.UUID{}),
|
|
})
|
|
}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"registrations": registrations,
|
|
"pending_registrations": registrations, // Fixed: Add for test compatibility and API clarity
|
|
"total": len(registrations),
|
|
})
|
|
}
|
|
|
|
func generateDeviceToken() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hashedToken, err := bcrypt.GenerateFromPassword(bytes, bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
token := base64.StdEncoding.EncodeToString(hashedToken)
|
|
|
|
return fmt.Sprintf("dev_%s", token), nil
|
|
}
|
|
|
|
func ValidateDeviceToken(hashedToken string, plainToken string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hashedToken), []byte(plainToken))
|
|
return err == nil
|
|
}
|