Implement device registration and management system for universal sync. Database Changes: - Add device queries to queries.sql (CRUD operations, registration, auth) - Add sync queue management queries - Add conflict resolution queries - Regenerate sqlc models with new device-related types Device Handler (devices.go): - InitiateRegistration: Start device registration with auth URL and QR code - CheckRegistrationStatus: Poll for registration approval - ListDevices: Get all devices for current user - GetDevice: Get specific device details - UpdateDevice: Update device settings (name, sync settings, frequency) - DeleteDevice: Remove device from account - ApproveDevice: User approves device registration via web - RejectDevice: Reject pending device registration - ListPendingRegistrations: Show all pending registrations - generateDeviceToken: Generate secure Bearer token for devices Device Authentication Middleware (device_auth.go): - Authenticate: Validate device Bearer tokens - RequirePermission: Check device permissions by type - hasPermission: Define permissions per device type - UpdateLastSeen: Auto-update device last_seen timestamp Configuration: - Add BaseURL field to Config for device setup URLs API Endpoints: POST /api/devices/register - Initiate device registration POST /api/devices/register/status - Check registration status GET /api/devices/approve/:id - Approve device (web UI) POST /api/devices/reject/:id - Reject device GET /api/devices - List user's devices GET /api/devices/:id - Get device details PUT /api/devices/:id - Update device settings DELETE /api/devices/:id - Delete device GET /api/devices/pending - List pending registrations Bruno API Collection: - Initiate Device Registration - Check Registration Status - List Devices - Get Device - Update Device - Delete Device Dependencies: - github.com/skip2/go-qrcode for QR code generation Device Types Supported: - koreader: Calibre-compatible sync - kobo: Kobo sync protocol - web: Web interface - mobile: Mobile apps Device Permissions: - sync:progress - sync:annotations - sync:metadata - device:manage (web only)
137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bookmann/internal/database"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type DeviceContext struct {
|
|
ID uuid.UUID
|
|
UserID uuid.UUID
|
|
DeviceName string
|
|
DeviceType string
|
|
DeviceIdentifier string
|
|
SyncEnabled bool
|
|
AutoSync bool
|
|
}
|
|
|
|
type DeviceAuthMiddleware struct {
|
|
db *database.Queries
|
|
}
|
|
|
|
func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware {
|
|
return &DeviceAuthMiddleware{
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
authHeader := c.Request().Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{
|
|
"error": "missing authorization header",
|
|
})
|
|
}
|
|
|
|
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 {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{
|
|
"error": "invalid device token",
|
|
})
|
|
}
|
|
|
|
if !device.SyncEnabled.Bool || !device.SyncEnabled.Valid {
|
|
return c.JSON(http.StatusForbidden, map[string]string{
|
|
"error": "device sync is disabled",
|
|
})
|
|
}
|
|
|
|
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", ctx)
|
|
c.Set("device_id", device.ID.Bytes)
|
|
c.Set("user_id", device.UserID.Bytes)
|
|
|
|
return next(c)
|
|
}
|
|
}
|
|
|
|
func (m *DeviceAuthMiddleware) RequirePermission(permission string) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
device, ok := c.Get("device").(DeviceContext)
|
|
if !ok {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{
|
|
"error": "device not authenticated",
|
|
})
|
|
}
|
|
|
|
if !m.hasPermission(device.DeviceType, permission) {
|
|
return c.JSON(http.StatusForbidden, map[string]string{
|
|
"error": "insufficient permissions",
|
|
})
|
|
}
|
|
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *DeviceAuthMiddleware) hasPermission(deviceType string, permission string) bool {
|
|
permissions := map[string][]string{
|
|
"koreader": {"sync:progress", "sync:annotations", "sync:metadata"},
|
|
"kobo": {"sync:progress", "sync:annotations", "sync:metadata"},
|
|
"web": {"sync:progress", "sync:annotations", "sync:metadata", "device:manage"},
|
|
"mobile": {"sync:progress", "sync:annotations", "sync:metadata"},
|
|
}
|
|
|
|
devicePerms, exists := permissions[deviceType]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
for _, p := range devicePerms {
|
|
if p == permission {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (m *DeviceAuthMiddleware) UpdateLastSeen(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
err := next(c)
|
|
|
|
deviceID, ok := c.Get("device_id").(uuid.UUID)
|
|
if ok {
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
m.db.UpdateDeviceLastSeen(c.Request().Context(), pgDeviceID)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|