Documentation
¶
Overview ¶
Package profiles implements device profiles: named buckets of preferences and limits. One profile is active per device at a time; the un-profiled state is the implicit "shared profile" — the device as it behaves when nobody is signed in, with global-config limits and unattributed history.
A profile's switch ID is a bearer credential: presenting it (by scanning the card it is written on, or by knowing its value) authorizes switching to that profile with no PIN on every path. Switch IDs are therefore only exposed over the API to privileged clients. The optional per-profile PIN protects the remaining path: switching by profile ID picked from the visible profile list. Leaving a profile is always free — PINs gate entry only.
Index ¶
- Constants
- Variables
- func GenerateSwitchID() (string, error)
- func HashPIN(pin string) (string, error)
- func VerifyPIN(pin, encoded string) bool
- type Broker
- type DataSwapCoordinator
- type LimitsResolver
- type Service
- func (s *Service) ActivateByID(profileID, pin string) (*models.ActiveProfile, error)
- func (s *Service) ActivateBySwitchID(switchID string) (*models.ActiveProfile, error)
- func (s *Service) Active() *models.ActiveProfile
- func (s *Service) Create(params *models.NewProfileParams) (*database.Profile, error)
- func (s *Service) Deactivate() error
- func (s *Service) Delete(profileID string) error
- func (s *Service) Get(profileID string) (*database.Profile, error)
- func (s *Service) List() ([]database.Profile, error)
- func (s *Service) ReconcileData()
- func (s *Service) RestoreOnBoot() error
- func (s *Service) SetDataSwap(c *DataSwapCoordinator)
- func (s *Service) Update(params *models.UpdateProfileParams) (*database.Profile, error)
- func (s *Service) VerifyByID(profileID, pin string) (*database.Profile, error)
- func (s *Service) VerifyBySwitchID(switchID string) (*database.Profile, error)
Constants ¶
const ( ProfileRoleAdmin = "admin" ProfileRoleMember = "member" )
Variables ¶
var ( // ErrPINRequired is returned when switching to a PIN-protected profile // without supplying a PIN. ErrPINRequired = errors.New("profile requires a PIN") // ErrPINIncorrect is returned when the supplied PIN does not match. ErrPINIncorrect = errors.New("incorrect PIN") // ErrPINRateLimited is returned when too many failed PIN attempts have // been made against a profile. ErrPINRateLimited = errors.New("too many PIN attempts, try again later") // ErrNotFound is returned when a profile does not exist. ErrNotFound = userdb.ErrProfileNotFound // ErrAdminPINRequired is returned when an administrator profile would // have no PIN protecting management authorization. ErrAdminPINRequired = errors.New("admin profiles require a PIN") // ErrLastAdmin is returned when deleting or demoting the final admin. ErrLastAdmin = userdb.ErrLastProfileAdmin // ErrInvalidRole is returned for unknown profile roles. ErrInvalidRole = errors.New("invalid profile role") )
var ErrInvalidPINFormat = errors.New("PIN must be 4 to 8 digits")
ErrInvalidPINFormat is returned when a PIN is not 4-8 digits.
Functions ¶
func GenerateSwitchID ¶
GenerateSwitchID returns a new random word-phrase switch ID, e.g. "corn-arm-truck". Uniqueness is enforced by the database; callers should retry on a unique-constraint conflict.
Types ¶
type Broker ¶
type Broker interface {
Subscribe(bufferSize int, methods ...string) (<-chan models.Notification, int)
Unsubscribe(id int)
}
Broker is the interface for subscribing to notifications.
type DataSwapCoordinator ¶
type DataSwapCoordinator struct {
// contains filtered or unexported fields
}
DataSwapCoordinator applies profile data swaps through a platform's ProfileDataSwapper. The profile switch itself is instant and never fails because of file operations: swaps run in a worker goroutine, switches wait for it only briefly, swaps while media is running are deferred until it stops, and queued targets coalesce to the most recent one.
func NewDataSwapCoordinator ¶
func NewDataSwapCoordinator( cfg *config.Instance, st *state.State, swapper platforms.ProfileDataSwapper, ) *DataSwapCoordinator
NewDataSwapCoordinator creates a coordinator for the given platform swapper. A nil swapper is valid and makes every method a no-op, so callers never need to branch on platform capability.
func (*DataSwapCoordinator) Reconcile ¶
func (c *DataSwapCoordinator) Reconcile()
Reconcile re-applies the current active profile's data state. Used at boot (after profile restore), when the swap_data setting changes, and by platform storage watchers when the mount table changes underneath us. Successful no-op reconciles are quiet; only errors notify.
func (*DataSwapCoordinator) RequestSwitch ¶
func (c *DataSwapCoordinator) RequestSwitch(ref platforms.ProfileRef)
RequestSwitch is called by the profiles service after the active profile changes. When no media is running it waits briefly for the swap so the common combo-card flow (switch then launch in one scan) launches with the new profile's data already mounted. While media runs, the swap is deferred until the media stops and the most recent target wins.
func (*DataSwapCoordinator) Start ¶
func (c *DataSwapCoordinator) Start(broker Broker, notificationsSend chan<- models.Notification)
Start subscribes to media lifecycle events and starts the apply worker.
func (*DataSwapCoordinator) Stop ¶
func (c *DataSwapCoordinator) Stop()
Stop shuts down the coordinator and waits for the worker to exit.
type LimitsResolver ¶
type LimitsResolver struct {
// contains filtered or unexported fields
}
LimitsResolver layers the active profile's playtime limit overrides over the global config. It satisfies the playtime.LimitsProvider interface. Reads come from the in-memory active-profile snapshot, never the database, so it is safe on the limit-check hot path.
An explicit (non-nil) profile field wins; a nil field inherits the global config value. A "0" duration string means explicitly unlimited. Warning intervals are device UX, not per-person policy, and always come from global config.
func NewLimitsResolver ¶
func NewLimitsResolver(cfg *config.Instance, st *state.State) *LimitsResolver
NewLimitsResolver creates a resolver over the global config and the service state holding the active profile.
func (*LimitsResolver) ActiveProfileID ¶
func (r *LimitsResolver) ActiveProfileID() string
ActiveProfileID returns the active profile's ID, or "" when no profile is active. The playtime limits manager uses this to scope daily usage accounting to the active profile's attributed history.
func (*LimitsResolver) DailyLimit ¶
func (r *LimitsResolver) DailyLimit() time.Duration
DailyLimit returns the active profile's daily limit override, or the global config value. Returns 0 for "no limit".
func (*LimitsResolver) PlaytimeLimitsEnabled ¶
func (r *LimitsResolver) PlaytimeLimitsEnabled() bool
PlaytimeLimitsEnabled returns the active profile's enabled override, or the global config value when no profile is active or it has no override.
func (*LimitsResolver) SessionLimit ¶
func (r *LimitsResolver) SessionLimit() time.Duration
SessionLimit returns the active profile's session limit override, or the global config value. Returns 0 for "no limit".
func (*LimitsResolver) WarningIntervals ¶
func (r *LimitsResolver) WarningIntervals() []time.Duration
WarningIntervals always returns the global config warning intervals.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service owns the device's profile lifecycle: CRUD, the active-profile state, and PIN-checked switching. All activation paths (API, ZapScript card scans, boot restore) go through here.
func NewService ¶
NewService creates a profiles service backed by the user database and service state.
func (*Service) ActivateByID ¶
func (s *Service) ActivateByID(profileID, pin string) (*models.ActiveProfile, error)
ActivateByID switches the device to a profile, enforcing its PIN if one is set. This is the API path; card scans use ActivateBySwitchID.
func (*Service) ActivateBySwitchID ¶
func (s *Service) ActivateBySwitchID(switchID string) (*models.ActiveProfile, error)
ActivateBySwitchID switches to a profile selected by switch ID without a PIN check. Switch IDs are bearer credentials: possessing the card or knowing its content is the authorization, on every path (scan, run API, profiles.switch). They are only readable via the API by privileged clients.
func (*Service) Active ¶
func (s *Service) Active() *models.ActiveProfile
Active returns the active profile snapshot, or nil when none is active.
func (*Service) Create ¶
Create creates a new profile with a generated profile ID and switch ID, hashing the PIN if one is given. The first profile is always an explicit administrator and therefore must have a PIN; later profiles default member.
func (*Service) Deactivate ¶
Deactivate clears the active profile. Leaving a profile is always free (PINs gate entry only); restricting what a profile-less device can do is handled by the require-profile launch setting.
func (*Service) Delete ¶
Delete removes a profile. If it is the active profile, the device deactivates (the persisted active state is cleared transactionally by the database layer).
func (*Service) ReconcileData ¶
func (s *Service) ReconcileData()
ReconcileData re-applies the active profile's data state, e.g. after the swap_data setting changes. No-op when data swapping is not wired.
func (*Service) RestoreOnBoot ¶
RestoreOnBoot restores the persisted active profile into service state. A dangling reference to a deleted profile is cleaned up silently.
func (*Service) SetDataSwap ¶
func (s *Service) SetDataSwap(c *DataSwapCoordinator)
SetDataSwap attaches the data swap coordinator. Optional: without it, profile switches change limits and attribution only.
func (*Service) Update ¶
Update applies an update to a profile. If the profile is currently active, the in-memory snapshot is refreshed so changed limits apply immediately.
func (*Service) VerifyByID ¶
VerifyByID checks a profile's PIN without switching to it. It shares the PIN rate limiter with activation, so it cannot be used to brute-force a PIN any faster than switching attempts could. Clients use this to gate their own ad-hoc UI items behind a profile credential; success grants nothing server-side.