Phase 2 Week 5: Device Registration & Management
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)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user