profiles

package
v2.16.1 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: GPL-3.0 Imports: 22 Imported by: 0

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

View Source
const (
	ProfileRoleAdmin  = "admin"
	ProfileRoleMember = "member"
)

Variables

View Source
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")
)
View Source
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

func GenerateSwitchID() (string, error)

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.

func HashPIN

func HashPIN(pin string) (string, error)

HashPIN validates and hashes a PIN for storage. The encoded form is "pbkdf2-sha256$<iterations>$<base64 salt>$<base64 key>".

func VerifyPIN

func VerifyPIN(pin, encoded string) bool

VerifyPIN reports whether pin matches the encoded hash. Malformed hashes verify as false.

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

func NewService(db *database.Database, st *state.State) *Service

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

func (s *Service) Create(params *models.NewProfileParams) (*database.Profile, error)

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

func (s *Service) Deactivate() error

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

func (s *Service) Delete(profileID string) error

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) Get

func (s *Service) Get(profileID string) (*database.Profile, error)

Get returns a profile by its profile ID.

func (*Service) List

func (s *Service) List() ([]database.Profile, error)

List returns all profiles.

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

func (s *Service) RestoreOnBoot() error

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

func (s *Service) Update(params *models.UpdateProfileParams) (*database.Profile, error)

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

func (s *Service) VerifyByID(profileID, pin string) (*database.Profile, error)

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.

func (*Service) VerifyBySwitchID

func (s *Service) VerifyBySwitchID(switchID string) (*database.Profile, error)

VerifyBySwitchID resolves a switch ID without switching. The switch ID is a bearer credential, so resolving it IS the verification — no PIN. Success grants nothing server-side.

Jump to

Keyboard shortcuts

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