device

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Index

Constants

View Source
const (
	EventSourceHeartbeat     = httpapi.Heartbeat
	EventSourceManual        = httpapi.Manual
	EventSourceExpiry        = httpapi.Expiry
	EventSourceLimitExceeded = httpapi.LimitExceeded
)
View Source
const (
	AttrKeyError            = logging.AttrKeyError
	AttrKeyAddressID        = "address_id"
	AttrKeyAddressIP        = "address_ip"
	AttrKeyClientIP         = "client_ip"
	AttrKeyCount            = "count"
	AttrKeyDeviceID         = "device_id"
	AttrKeyDeviceName       = "device_name"
	AttrKeyAddressEventType = "address_event_type"
)

Slog attribute key names for the device domain. Use these constants when logging so keys are consistent and typo-safe across handlers and services.

View Source
const APIKeyPrefix = "wdk_"

Variables

View Source
var (
	ErrDeviceNotFound          = errors.New("device not found")
	ErrOwnerNotFound           = errors.New("device owner not found")
	ErrNoAPIKey                = errors.New("device has no API key")
	ErrDuplicateDeviceName     = errors.New("device name already in use")
	ErrAddressNotFound         = errors.New("device address not found")
	ErrInvalidIPFormat         = errors.New("invalid IP address format")
	ErrInvalidDeviceIP         = errors.New("ip address is not valid for device registration")
	ErrTrustedProxyIPRejected  = errors.New("ip address belongs to trusted proxy")
	ErrAddressNotOwnedByDevice = errors.New("address is not owned by the device")
	ErrInvalidAPIKey           = errors.New("invalid api key")
	ErrInvalidDeviceType       = errors.New("invalid device type")
	ErrInvalidDeviceName       = errors.New("device name must be between 1 and 50 characters")
	ErrDescriptionTooLong      = errors.New("description must be 200 characters or fewer")
	ErrIconTooLong             = errors.New("icon must be 80 characters or fewer")
)
View Source
var AllowedDeviceTypes = []DeviceType{
	DeviceTypeStatic,
	DeviceTypeMobile,
}

AllowedDeviceTypes is the canonical ordered list of valid device types.

View Source
var DeviceTypeLabels = map[DeviceType]string{
	DeviceTypeStatic: "Static",
	DeviceTypeMobile: "Mobile",
}

DeviceTypeLabels maps each device type to its display label.

Functions

func GenerateAPIKey added in v0.3.0

func GenerateAPIKey() (rawKey string, keyHash string, keyPrefix string, error error)

GenerateAPIKey generates a new API key and returns the raw key (to send to user), the key hash (to store in DB), and the key prefix (for display).

func HashAPIKey added in v0.3.0

func HashAPIKey(rawKey string) string

HashAPIKey hashes the API key using SHA-256 and returns base64url encoded hash. This mirrors the pattern used in auth.hashRawToken.

func ParseAndValidateIP added in v0.3.0

func ParseAndValidateIP(ipInput string) (netip.Addr, error)

ParseAndValidateIP parses and validates that the given string is a valid IPv4 or IPv6 address. It ignores the port if present and only cares about the IP component. TODO: Make private once the address_test go through the NewCreateAddressParams

func PrincipalDeviceContextMiddleware

func PrincipalDeviceContextMiddleware(apiKeyAuthenticator APIKeyAuthenticator) func(http.Handler) http.Handler

PrincipalDeviceContextMiddleware resolves the api key into a Device Principal and injects it into the context.

func WithPrincipal

func WithPrincipal(ctx context.Context, principal Principal) context.Context

Types

type APIKeyAuthenticator

type APIKeyAuthenticator interface {
	Authenticate(ctx context.Context, rawKey string) (*Principal, error)
}

APIKeyAuthenticator defines the interface for authenticating device API keys.

type Address

type Address struct {
	ID        AddressID   `db:"id"`
	DeviceID  DeviceID    `db:"device_id"`
	IP        string      `db:"ip"`
	IsEnabled bool        `db:"is_enabled"`
	Source    EventSource `db:"source"`
	CreatedAt time.Time   `db:"created_at"`
	UpdatedAt time.Time   `db:"updated_at"`
}

Address represents an address row with its current enabled/disabled state and metadata.

type AddressEvent

type AddressEvent struct {
	Type       EventType
	AddressID  AddressID
	DeviceID   DeviceID
	OccurredAt time.Time
}

func NewAddressEvent

func NewAddressEvent(address *Address, eventType EventType) AddressEvent

func NewDeviceEvent added in v0.3.0

func NewDeviceEvent(deviceID DeviceID, eventType EventType) AddressEvent

func (AddressEvent) IsAddressEnabled

func (e AddressEvent) IsAddressEnabled() bool

IsAddressEnabled returns true for Created, Enabled, and Refreshed events (address is enabled); false for Disabled.

func (AddressEvent) LogValue

func (e AddressEvent) LogValue() slog.Value

type AddressEventBucket

type AddressEventBucket struct {
	Timestamp   database.DBTime `db:"bucket"`
	ActiveCount int             `db:"active_count"`
	EventCount  int             `db:"event_count"`
}

AddressEventBucket holds aggregated activity data for one time period. Timestamp uses DBTime because SQLite's strftime returns TEXT even for DATETIME columns, and DBTime handles the multi-format scanning.

type AddressHistory

type AddressHistory struct {
	Buckets     []AddressEventBucket
	Events      []AddressStateChange
	TotalEvents int
	QueryLimit  int // effective limit used for the query, needed for cursor logic
}

AddressHistory holds the complete history response.

type AddressHistoryQuery

type AddressHistoryQuery struct {
	From        time.Time
	To          time.Time
	Granularity timebucket.Granularity
	DeviceIDs   []DeviceID // empty = all devices
	Source      *string
	IsEnabled   *bool
	IP          *string
	BeforeID    *int64 // cursor for events pagination
	Limit       int    // events limit (default 50, max 200)
	IncludeAll  bool   // when false (default), only state-change events are returned
}

AddressHistoryQuery encapsulates all filters and pagination for history queries.

func (*AddressHistoryQuery) Validate

func (q *AddressHistoryQuery) Validate() error

Validate normalizes defaults and validates business rules on the query. Must be called before passing the query to the repository.

type AddressID

type AddressID int64

func (AddressID) Int64

func (id AddressID) Int64() int64

func (AddressID) String

func (id AddressID) String() string

type AddressObserver

type AddressObserver interface {
	OnAddressEvent(ctx context.Context, event AddressEvent)
}

type AddressStateChange

type AddressStateChange struct {
	ID         int64       `db:"id"`
	CreatedAt  time.Time   `db:"created_at"`
	IP         string      `db:"ip"`
	IsEnabled  bool        `db:"is_enabled"`
	Source     EventSource `db:"source"`
	DeviceID   DeviceID    `db:"device_id"`
	DeviceName string      `db:"device_name"`
}

AddressStateChange represents a single recorded state change from the address_events audit table.

type CreateAddressParams

type CreateAddressParams struct {
	DeviceID DeviceID
	IP       netip.Addr
}

CreateAddressParams holds only what is necessary to create an address.

func NewCreateAddressParams

func NewCreateAddressParams(deviceID DeviceID, ipAddress string, trustedProxy netip.Addr) (CreateAddressParams, error)

type CreateDeviceParams

type CreateDeviceParams struct {
	Name       string
	OwnerID    auth.UserID
	DeviceType string // optional; defaults to "static" when empty
}

type Device

type Device struct {
	ID          DeviceID         `db:"id"`
	Name        string           `db:"name"`
	DeviceType  DeviceType       `db:"device_type"`
	Description *string          `db:"description"`
	Icon        *string          `db:"icon"`
	CreatedAt   time.Time        `db:"created_at"`
	UpdatedAt   time.Time        `db:"updated_at"`
	DeletedAt   *time.Time       `db:"deleted_at"`
	KeyPrefix   *string          `db:"key_prefix"`
	LastSeenAt  *database.DBTime `db:"last_seen_at"`
	OwnerID     auth.UserID      `db:"owner_id"`
}

func (*Device) Update added in v0.2.0

func (d *Device) Update(name *string, deviceType *string, description **string, icon **string, ownerID *auth.UserID) error

Update applies patch inputs to the device in-place, validating each field. Fields with a nil pointer are left unchanged. The device is not mutated unless all validations pass.

  • name: nil = keep current; non-nil = rename (validated)
  • deviceType: nil = keep current; non-nil = set type (validated)
  • description: nil = keep current; non-nil ptr to nil = clear; non-nil ptr to value = set
  • icon: same semantics as description

type DeviceID

type DeviceID int64

func (DeviceID) Int64

func (id DeviceID) Int64() int64

func (DeviceID) String

func (id DeviceID) String() string

type DeviceType added in v0.2.0

type DeviceType string

DeviceType describes the network behaviour of a device. Keeping it really simple, in the future rules could be created on top of it. Main usage now is for filtering. For example static devices do not need address lease but mobile do, static ones can have different geoip rules...

const (
	DeviceTypeStatic DeviceType = "static"
	DeviceTypeMobile DeviceType = "mobile"
)

type EventSource

type EventSource = httpapi.AddressEventSource

EventSource is an alias for the API-generated type, making openapi.yaml the single source of truth for valid values.

type EventType

type EventType string
const (
	EventTypeAddressCreated         EventType = "address_created"
	EventTypeAddressEnabled         EventType = "address_enabled"
	EventTypeAddressRefreshed       EventType = "address_refreshed"
	EventTypeAddressDisabled        EventType = "address_disabled"
	EventTypeDeviceOwnershipChanged EventType = "device_ownership_changed"
)

type HTTPHandler

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

func NewHTTPHandler

func NewHTTPHandler(service *Service, logger *slog.Logger) *HTTPHandler

func (*HTTPHandler) APIKeyAuthenticator

func (h *HTTPHandler) APIKeyAuthenticator() APIKeyAuthenticator

func (*HTTPHandler) AddAddress

func (*HTTPHandler) DeleteDeviceAPIKey added in v0.3.0

func (*HTTPHandler) ListDeviceTypes added in v0.2.0

func (*HTTPHandler) UpdateDevice added in v0.2.0

type IPEntry

type IPEntry struct {
	IP        string      `db:"ip"`
	DeviceID  DeviceID    `db:"device_id"`
	AddressID AddressID   `db:"address_id"`
	UserID    auth.UserID `db:"user_id"`
}

IPEntry associates an enabled IP address with the device, address, and owning user. All enabled rows are returned (multiple per IP when devices share an address); the policy layer merges them with deny-wins intersection.

type Principal

type Principal struct {
	DeviceID DeviceID
}

func NewPrincipal

func NewPrincipal(deviceID DeviceID) *Principal

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (*Principal, bool)

func PrincipalFromDevice

func PrincipalFromDevice(device *Device) *Principal

type Repository

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

func NewRepository

func NewRepository(db *database.DB) *Repository

func (*Repository) CheckAddressOwnership

func (r *Repository) CheckAddressOwnership(ctx context.Context, deviceID DeviceID, addressID AddressID) error

func (*Repository) CreateAddress

func (r *Repository) CreateAddress(ctx context.Context, params CreateAddressParams, source EventSource) (*Address, error)

func (*Repository) CreateDevice

func (r *Repository) CreateDevice(ctx context.Context, params CreateDeviceParams) (*Device, error)

func (*Repository) DeleteAPIKey added in v0.3.0

func (r *Repository) DeleteAPIKey(ctx context.Context, deviceID DeviceID) error

func (*Repository) DeleteDevice

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

func (*Repository) DisableAddress

func (r *Repository) DisableAddress(ctx context.Context, addressID AddressID) (*Address, error)

func (*Repository) DisableAddresses

func (r *Repository) DisableAddresses(ctx context.Context, addressIDs []AddressID, source EventSource) ([]Address, error)

func (*Repository) EnableAddress

func (r *Repository) EnableAddress(ctx context.Context, addressID AddressID, source EventSource) (*Address, error)

func (*Repository) GetAddress

func (r *Repository) GetAddress(ctx context.Context, addressID AddressID) (*Address, error)

func (*Repository) GetAddressForDeviceByIP

func (r *Repository) GetAddressForDeviceByIP(ctx context.Context, deviceID DeviceID, ip netip.Addr) (*Address, error)

func (*Repository) GetAddressHistory

func (r *Repository) GetAddressHistory(ctx context.Context, q AddressHistoryQuery) (AddressHistory, error)

func (*Repository) GetDevice

func (r *Repository) GetDevice(ctx context.Context, id DeviceID) (*Device, error)

func (*Repository) GetDeviceByAPIKeyHash

func (r *Repository) GetDeviceByAPIKeyHash(ctx context.Context, keyHash string) (*Device, error)

func (*Repository) GetEnabledAddressesForDevice added in v0.2.0

func (r *Repository) GetEnabledAddressesForDevice(ctx context.Context, deviceID DeviceID) ([]Address, error)

GetEnabledAddressesForDevice returns all enabled addresses for a device, ordered by updated_at DESC.

func (*Repository) GetEnabledIPEntries

func (r *Repository) GetEnabledIPEntries(ctx context.Context) ([]IPEntry, error)

func (*Repository) RefreshAddress

func (r *Repository) RefreshAddress(ctx context.Context, addressID AddressID, source EventSource) (*Address, error)

RefreshAddress records activity for an already-enabled address

func (*Repository) UpdateDevice added in v0.2.0

func (r *Repository) UpdateDevice(ctx context.Context, device *Device) (*Device, error)

func (*Repository) UpsertAPIKey added in v0.3.0

func (r *Repository) UpsertAPIKey(ctx context.Context, deviceID DeviceID, keyHash string, keyPrefix string) error

type Service

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

func NewService

func NewService(repo repository, transactor transactor, logger *slog.Logger, trustedProxy netip.Addr) *Service

func (*Service) AddAddressObserver

func (s *Service) AddAddressObserver(o AddressObserver)

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, rawKey string) (*Principal, error)

func (*Service) CreateDevice

func (s *Service) CreateDevice(ctx context.Context, principal *auth.Principal, name string, requestedOwnerID *auth.UserID) (*Device, error)

func (*Service) CreateDeviceWithAPIKey added in v0.3.0

func (s *Service) CreateDeviceWithAPIKey(ctx context.Context, name string, ownerID auth.UserID) (DeviceID, string, error)

CreateDeviceWithAPIKey creates a new device and assigns it an API key, all within a single transaction. It is the entry point for the registration domain when claiming an invite. Returns the new device ID and the plaintext API key (one-time; never stored after this call).

func (*Service) DeleteAPIKey added in v0.3.0

func (s *Service) DeleteAPIKey(ctx context.Context, deviceID DeviceID) error

func (*Service) DeleteDevice

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

func (*Service) DisableAddress

func (s *Service) DisableAddress(ctx context.Context, deviceID DeviceID, addressID AddressID) (*Address, error)

func (*Service) DisableAddresses

func (s *Service) DisableAddresses(ctx context.Context, addressIDs []AddressID, source EventSource) error

func (*Service) GetAddressHistory

func (s *Service) GetAddressHistory(ctx context.Context, query AddressHistoryQuery) (AddressHistory, error)

func (*Service) GetDevice

func (s *Service) GetDevice(ctx context.Context, deviceID DeviceID) (*Device, error)

func (*Service) GetEnabledAddressesForDevice added in v0.2.0

func (s *Service) GetEnabledAddressesForDevice(ctx context.Context, deviceID DeviceID) ([]Address, error)

GetEnabledAddressesForDevice returns all enabled addresses for a device, ordered by updated_at DESC.

func (*Service) GetEnabledIPEntries

func (s *Service) GetEnabledIPEntries(ctx context.Context) ([]IPEntry, error)

func (*Service) RegenerateAPIKey

func (s *Service) RegenerateAPIKey(ctx context.Context, deviceID DeviceID) (*Device, string, error)

func (*Service) RegisterAddressActivity

func (s *Service) RegisterAddressActivity(ctx context.Context, deviceID DeviceID, inputIP string, source EventSource) (*Address, EventType, error)

func (*Service) UpdateDevice added in v0.2.0

func (s *Service) UpdateDevice(ctx context.Context, deviceID DeviceID, input UpdateDeviceInput) (*Device, error)

type UpdateDeviceInput added in v0.2.0

type UpdateDeviceInput struct {
	Name        *string
	DeviceType  *string
	Description **string
	Icon        **string
	OwnerID     *auth.UserID
}

UpdateDeviceInput carries the raw nullable API values for a device profile update. nil pointer = field was absent in the request (leave unchanged). For Description and Icon, **string semantics apply: nil = absent, *nil = clear, *&s = set.

Jump to

Keyboard shortcuts

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