device

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package device manages device registration, pairing, and push subscriptions.

Index

Constants

View Source
const (
	// PairingOTPTTL is the time-to-live for pairing OTPs.
	PairingOTPTTL = 5 * time.Minute
)

Variables

View Source
var (
	ErrDeviceNotFound        = fmt.Errorf("device not found")
	ErrPairingCodeInvalid    = fmt.Errorf("pairing code invalid or expired")
	ErrAtLeastOneTopic       = fmt.Errorf("at least one topic is required")
	ErrInvalidTopicSelection = fmt.Errorf("invalid topic selection")
	ErrInvalidPushEndpoint   = fmt.Errorf("invalid push endpoint")
	ErrInvalidAgeRecipient   = fmt.Errorf("invalid age recipient")
	ErrInvalidDeviceToken    = fmt.Errorf("invalid device token")
)

Sentinel errors.

Functions

This section is empty.

Types

type CreateDeviceRequest

type CreateDeviceRequest struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Topics      []string `json:"topics"`
}

CreateDeviceRequest is the HTTP request for creating a device.

func (*CreateDeviceRequest) Validate

func (r *CreateDeviceRequest) Validate() []error

Validate checks the create device request fields.

type CreatedDeviceResponse

type CreatedDeviceResponse struct {
	Device      DeviceResponse `json:"device"`
	PairingCode string         `json:"pairing_code"`
	PairingURL  string         `json:"pairing_url"`
	QRCode      string         `json:"qr_code"`
	ExpiresAt   time.Time      `json:"expires_at"`
}

CreatedDeviceResponse is the HTTP response for a newly created device (includes pairing code + QR).

type Device

type Device struct {
	ID              string        `db:"id"`
	UserID          string        `db:"user_id"`
	Name            string        `db:"name"`
	Description     string        `db:"description"`
	IsActive        bool          `db:"is_active"`
	PairingStatus   PairingStatus `db:"pairing_status"`
	DeviceTokenHash *string       `db:"device_token_hash"`
	CreatedAt       int64         `db:"created_at"`
	UpdatedAt       int64         `db:"updated_at"`
	// Derived from LEFT JOIN push_subscriptions (not a column in devices table).
	SubCreatedAt    *int64  `db:"sub_created_at"`
	SubAgeRecipient *string `db:"sub_age_recipient"`
}

Device is the DB row type for the devices table.

type DeviceKeyDescriptor

type DeviceKeyDescriptor struct {
	DeviceID                string    `json:"device_id"`
	DeviceName              string    `json:"device_name"`
	PairedAt                time.Time `json:"paired_at"`
	AgeRecipient            string    `json:"age_recipient"`
	AgeRecipientFingerprint string    `json:"age_recipient_fingerprint"`
}

DeviceKeyDescriptor is the HTTP/API representation of a paired device key.

func ToDeviceKeyDescriptor

func ToDeviceKeyDescriptor(d *Device) DeviceKeyDescriptor

ToDeviceKeyDescriptor converts a paired device row to a device-key descriptor.

type DeviceKeysResponse

type DeviceKeysResponse struct {
	Data []DeviceKeyDescriptor `json:"data"`
}

DeviceKeysResponse is the HTTP response for paired device keys.

type DeviceResponse

type DeviceResponse struct {
	ID                      string        `json:"id"`
	Name                    string        `json:"name"`
	Description             string        `json:"description"`
	IsActive                bool          `json:"is_active"`
	PairingStatus           PairingStatus `json:"pairing_status"`
	PairedAt                *time.Time    `json:"paired_at"`
	AgeRecipient            *string       `json:"age_recipient"`
	AgeRecipientFingerprint *string       `json:"age_recipient_fingerprint"`
	TopicIDs                []string      `json:"topic_ids"`
	CreatedAt               time.Time     `json:"created_at"`
	UpdatedAt               time.Time     `json:"updated_at"`
}

DeviceResponse is the HTTP response for a single device.

func ToDeviceResponse

func ToDeviceResponse(d *Device, topicIDs []string) DeviceResponse

ToDeviceResponse converts a Device DB row to a DeviceResponse.

type DeviceTopic

type DeviceTopic struct {
	DeviceID  string `db:"device_id"`
	TopicID   string `db:"topic_id"`
	CreatedAt int64  `db:"created_at"`
}

DeviceTopic is the DB row type for the device_topics table.

type DevicesListResponse

type DevicesListResponse struct {
	Data []DeviceResponse `json:"data"`
}

DevicesListResponse is the HTTP response for a list of devices.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler handles device HTTP requests.

func NewHandler

func NewHandler(svc *Service, hiveURL string, logger *slog.Logger) *Handler

NewHandler creates a new device handler.

func (*Handler) CreateDevice

func (h *Handler) CreateDevice(w http.ResponseWriter, r *http.Request)

CreateDevice handles POST /v1/devices.

func (*Handler) DeleteDevice

func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request)

DeleteDevice handles DELETE /v1/devices/{deviceID}.

func (*Handler) GetDevices

func (h *Handler) GetDevices(w http.ResponseWriter, r *http.Request)

GetDevices handles GET /v1/devices.

func (*Handler) Pair

func (h *Handler) Pair(w http.ResponseWriter, r *http.Request)

Pair handles POST /v1/pairing (public endpoint).

func (*Handler) PairingStatus

func (h *Handler) PairingStatus(w http.ResponseWriter, r *http.Request)

PairingStatus handles GET /v1/pairing/{deviceID} (public, device-token-authenticated).

func (*Handler) RegeneratePairingOTP

func (h *Handler) RegeneratePairingOTP(w http.ResponseWriter, r *http.Request)

RegeneratePairingOTP handles POST /v1/devices/{deviceID}/pairing-code.

func (*Handler) UnpairDevice

func (h *Handler) UnpairDevice(w http.ResponseWriter, r *http.Request)

UnpairDevice handles POST /v1/devices/{deviceID}/unpair.

func (*Handler) UpdateDevice

func (h *Handler) UpdateDevice(w http.ResponseWriter, r *http.Request)

UpdateDevice handles PATCH /v1/devices/{deviceID}.

type PairRequest

type PairRequest struct {
	PairingCode  string `json:"pairing_code"`
	Endpoint     string `json:"endpoint"`
	P256dh       string `json:"p256dh"`
	Auth         string `json:"auth"`
	AgeRecipient string `json:"age_recipient"`
}

PairRequest is the HTTP request for pairing a device with a push subscription.

func (*PairRequest) Validate

func (r *PairRequest) Validate() []error

Validate checks the pair request fields.

type PairResponse

type PairResponse struct {
	DeviceID    string `json:"device_id"`
	DeviceToken string `json:"device_token"`
}

PairResponse is the HTTP response returned after a successful pairing.

type PairingCode

type PairingCode struct {
	CodeHash     string `db:"code_hash"`
	DeviceID     string `db:"device_id"`
	ExpiresAt    int64  `db:"expires_at"`
	UsedAt       *int64 `db:"used_at"`
	AttemptCount int    `db:"attempt_count"`
	CreatedAt    int64  `db:"created_at"`
}

PairingCode is the DB row type for the device_pairing_codes table.

type PairingCodeResponse

type PairingCodeResponse struct {
	PairingCode string    `json:"pairing_code"`
	PairingURL  string    `json:"pairing_url"`
	QRCode      string    `json:"qr_code"`
	ExpiresAt   time.Time `json:"expires_at"`
}

PairingCodeResponse is the HTTP response for a regenerated pairing code.

type PairingStatus

type PairingStatus string

PairingStatus is the canonical device pairing state exposed to clients.

const (
	PairingStatusPending          PairingStatus = "pending"
	PairingStatusPaired           PairingStatus = "paired"
	PairingStatusUnpaired         PairingStatus = "unpaired"
	PairingStatusSubscriptionGone PairingStatus = "subscription_gone"
)

type PairingStatusResponse

type PairingStatusResponse struct {
	DeviceID      string        `json:"device_id"`
	PairingStatus PairingStatus `json:"pairing_status"`
}

PairingStatusResponse is the public Hive pairing status payload.

type PushSubscription

type PushSubscription struct {
	DeviceID     string `db:"device_id"`
	Endpoint     string `db:"endpoint"`
	P256dh       string `db:"p256dh"`
	Auth         string `db:"auth"`
	AgeRecipient string `db:"age_recipient"`
	CreatedAt    int64  `db:"created_at"`
	UpdatedAt    int64  `db:"updated_at"`
}

PushSubscription is the DB row type for the push_subscriptions table.

type Repository

type Repository struct {
	// contains filtered or unexported fields
}

Repository handles all device-related DB queries.

func NewRepository

func NewRepository(db *sqlx.DB) *Repository

NewRepository creates a new device Repository.

func (*Repository) AddTopicToDevice

func (r *Repository) AddTopicToDevice(ctx context.Context, deviceID, topicID string) error

AddTopicToDevice inserts a device-topic association.

func (*Repository) ClearPushSubscriptionWithStatus

func (r *Repository) ClearPushSubscriptionWithStatus(ctx context.Context, deviceID string, status PairingStatus) error

ClearPushSubscriptionWithStatus deletes the subscription and updates pairing_status atomically.

func (*Repository) ConsumePairingCode

func (r *Repository) ConsumePairingCode(ctx context.Context, codeHash string, sub PushSubscription, deviceTokenHash string) (string, error)

ConsumePairingCode atomically marks a code as used, stores the push subscription, and sets paired_at. All operations run inside a single transaction to prevent burning the code without completing pairing.

func (*Repository) CreateDevice

func (r *Repository) CreateDevice(ctx context.Context, id, userID, name, description string) error

CreateDevice inserts a new device record.

func (*Repository) CreateDeviceWithTopicsAndPairingCode

func (r *Repository) CreateDeviceWithTopicsAndPairingCode(ctx context.Context, id, userID, name, description string, topicIDs []string, codeHash string, expiresAt int64) error

CreateDeviceWithTopicsAndPairingCode atomically creates a device, its topic associations, and its pairing code.

func (*Repository) CreatePairingCode

func (r *Repository) CreatePairingCode(ctx context.Context, codeHash, deviceID string, expiresAt int64) error

CreatePairingCode inserts a new pairing code record.

func (*Repository) DeleteDevice

func (r *Repository) DeleteDevice(ctx context.Context, deviceID string) error

DeleteDevice soft-deletes a device by setting is_active = 0.

func (*Repository) DeletePushSubscription

func (r *Repository) DeletePushSubscription(ctx context.Context, deviceID string) error

DeletePushSubscription deletes a push subscription by device ID.

func (*Repository) DeleteTopicFromDevice

func (r *Repository) DeleteTopicFromDevice(ctx context.Context, deviceID, topicID string) error

DeleteTopicFromDevice removes a device-topic association.

func (*Repository) GetActivePairingCode

func (r *Repository) GetActivePairingCode(ctx context.Context, codeHash string) (*PairingCode, error)

GetActivePairingCode returns the active (unused, not expired) pairing code matching the hash. Returns nil, nil if not found.

func (*Repository) GetDeviceByID

func (r *Repository) GetDeviceByID(ctx context.Context, deviceID string) (*Device, error)

GetDeviceByID returns an active device by ID, or nil if not found.

func (*Repository) GetDeviceByIDAndTokenHash

func (r *Repository) GetDeviceByIDAndTokenHash(ctx context.Context, deviceID, tokenHash string) (*Device, error)

GetDeviceByIDAndTokenHash returns an active device by ID and token hash, or nil if not found.

func (*Repository) GetDeviceByIDAndUser

func (r *Repository) GetDeviceByIDAndUser(ctx context.Context, deviceID, userID string) (*Device, error)

GetDeviceByIDAndUser returns a device by ID scoped to a user, or nil if not found.

func (*Repository) GetDeviceKeysByUser

func (r *Repository) GetDeviceKeysByUser(ctx context.Context, userID string) ([]Device, error)

GetDeviceKeysByUser returns paired devices with their age public keys for a user.

func (*Repository) GetDeviceTopicIDs

func (r *Repository) GetDeviceTopicIDs(ctx context.Context, deviceID string) ([]string, error)

GetDeviceTopicIDs returns all topic IDs associated with a device.

func (*Repository) GetPushSubscriptionsByUserAndTopic

func (r *Repository) GetPushSubscriptionsByUserAndTopic(ctx context.Context, userID, topicName string) ([]PushSubscription, error)

GetPushSubscriptionsByUserAndTopic returns push subscriptions for all active devices subscribed to a given topic name for a given user.

func (*Repository) IncrementAndGetAttempts

func (r *Repository) IncrementAndGetAttempts(ctx context.Context, codeHash string) (int, error)

IncrementAndGetAttempts atomically increments the attempt counter and returns the new count.

func (*Repository) InvalidatePairingCodes

func (r *Repository) InvalidatePairingCodes(ctx context.Context, deviceID string) error

InvalidatePairingCodes marks all unused pairing codes for a device as used.

func (*Repository) ListDevicesByUser

func (r *Repository) ListDevicesByUser(ctx context.Context, userID string) ([]Device, error)

ListDevicesByUser returns all active devices for a user, ordered by created_at DESC.

func (*Repository) UpdateDevice

func (r *Repository) UpdateDevice(ctx context.Context, deviceID, name, description string) error

UpdateDevice updates the name, description, and updated_at of a device.

func (*Repository) UpdateDeviceWithTopics

func (r *Repository) UpdateDeviceWithTopics(ctx context.Context, userID, deviceID, name, description string, topicIDs []string) error

UpdateDeviceWithTopics atomically updates device fields and replaces topic associations.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service handles device business logic.

func NewService

func NewService(repo *Repository, topicValidator TopicValidator, log *slog.Logger) *Service

NewService creates a new device Service.

func (*Service) CreateDevice

func (s *Service) CreateDevice(ctx context.Context, userID, name, description string, topicIDs []string) (*Device, string, time.Time, error)

CreateDevice creates a device with topics and a pairing OTP. Returns device, raw OTP, and expiry.

func (*Service) DeleteDevice

func (s *Service) DeleteDevice(ctx context.Context, userID, deviceID string) error

DeleteDevice validates ownership and soft-deletes the device.

func (*Service) DeletePushSubscription

func (s *Service) DeletePushSubscription(ctx context.Context, deviceID string) error

DeletePushSubscription deletes a push subscription by device ID.

func (*Service) GetDeviceKeysByUser

func (s *Service) GetDeviceKeysByUser(ctx context.Context, userID string) ([]DeviceKeyDescriptor, error)

GetDeviceKeysByUser returns paired devices with their age public keys for a user.

func (*Service) GetPairingStatus

func (s *Service) GetPairingStatus(ctx context.Context, deviceID, deviceToken string) (*PairingStatusResponse, error)

GetPairingStatus returns the canonical pairing status for a device ID, authenticated by device token.

func (*Service) GetSubscribedDevices

func (s *Service) GetSubscribedDevices(ctx context.Context, userID, topicName string) ([]PushSubscription, error)

GetSubscribedDevices returns push subscriptions for devices subscribed to a topic for a user.

func (*Service) ListDevices

func (s *Service) ListDevices(ctx context.Context, userID string) ([]DeviceResponse, error)

ListDevices returns all active devices for a user with their topics.

func (*Service) MarkSubscriptionGone

func (s *Service) MarkSubscriptionGone(ctx context.Context, deviceID string) error

MarkSubscriptionGone removes a push subscription invalidated by the push provider.

func (*Service) Pair

func (s *Service) Pair(ctx context.Context, otp, endpoint, p256dh, authKey, ageRecipient string) (string, string, error)

Pair verifies the OTP and consumes it atomically with push subscription storage.

func (*Service) RegeneratePairingOTP

func (s *Service) RegeneratePairingOTP(ctx context.Context, userID, deviceID string) (string, time.Time, error)

RegeneratePairingOTP validates ownership, invalidates old codes, and creates a new OTP.

func (*Service) UnpairDevice

func (s *Service) UnpairDevice(ctx context.Context, userID, deviceID string) error

UnpairDevice validates ownership and deletes the push subscription.

func (*Service) UpdateDevice

func (s *Service) UpdateDevice(ctx context.Context, userID, deviceID, name, description string, topicIDs []string) error

UpdateDevice validates ownership and updates the device name, description, and topics.

type TopicValidator

type TopicValidator interface {
	ValidateTopicIDs(ctx context.Context, userID string, topicIDs []string) error
}

TopicValidator verifies that topic IDs belong to the given user.

type UpdateDeviceRequest

type UpdateDeviceRequest struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Topics      []string `json:"topics"`
}

UpdateDeviceRequest is the HTTP request for updating a device.

func (*UpdateDeviceRequest) Validate

func (r *UpdateDeviceRequest) Validate() []error

Validate checks the update device request fields.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL