Settings had only an ask-on-start toggle and a button that bounced to the
picker, so there was nowhere to actually turn a PIN on or off -- the one
part of this feature that genuinely is configuration rather than a
front-door action.
Scoped to the active profile on purpose. Letting a signed-in session set
a PIN on someone else's profile is an escalation path with no real use:
a PIN-less profile could be locked by whoever is standing at the device,
and its owner pushed down the password route to get back into their own
account. Other profiles are listed read-only so a parent can see which
are protected; adding and removing them stays on the picker, which
settings now deep-links into with ?manage=1 rather than growing a second
copy of the tile list.
Form validation is extracted to pinForm.ts and unit-tested -- shape only,
so the form can say what is wrong before submitting. Rust validates again
and remains the only thing that ever compares a PIN.
Switching was reachable only from Settings, which is the wrong place for
it: on a shared device changing who is watching is a frequent, front-door
action, not a configuration change buried three screens deep.
It sits directly under the identity block rather than among the
destinations (Downloads/Settings/Display), because it answers "who is
this?" and not "where do I go?". Shown even with one account, since the
picker is also where a second is added -- gating it on a second profile
existing would leave no way in from here.
The picker also gains a Back affordance when a session is already live.
Reaching it from the menu and changing your mind -- or failing a PIN on
someone else's tile -- previously had no way back to the session you
still had. At startup there is nothing behind it, so it stays hidden.
A shared device can hold several accounts from the same server and switch
between them in a couple of taps. A profile can be locked behind a 4-8
digit PIN; one without a PIN is one tap away. Forgetting a PIN falls
through to the account's own Jellyfin password, so there is no reset flow
and no recovery secret to store.
Opt-in by construction: a single account with no PIN starts, plays and
downloads exactly as before, and never sees a picker.
Two decisions worth keeping:
- Switching is not logging out. auth_logout invalidates the token
server-side, which is precisely what a switch must not do, or every
switch back would cost a password. The switch runs as a plan
(profiles/switch.rs) so the teardown *ordering* is unit-testable with
no player and no server -- a straggler reporting after the active user
flips would attribute one account's viewing to another, silently.
- The PIN gates switching, not the token at rest. Wrapping each token
with its PIN would leave a locked profile unable to resume its own
downloads or drain its own sync queue until somebody typed the code,
which on a device that reboots nightly costs more than it defends
against a four-digit secret. auth_initialize does refuse to restore a
PIN-protected session, so the gate is on the session rather than on
which screen is shown.
"Child account" is not modelled anywhere -- a child's profile is simply
one with no PIN. The frontend renders an opaque unlockMethod and never
compares a PIN, counts an attempt or infers a role.
Migration 024 adds user_pins, user_item_visibility, user_libraries and
download_grants, and backfills the existing user so an upgrade does not
blank its library. The visibility and grant tables are the schema half of
the cache-scoping and shared-download work; the read-path enforcement is
still to come (see docs/specs/multi-user-profiles.md).