feat(profiles): multi-user profiles with PIN switching
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).
This commit is contained in:
@@ -606,6 +606,66 @@ function createAuthStore() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild this store's view of the world after the backend has switched
|
||||
* profiles.
|
||||
*
|
||||
* The backend already flipped the active user, adopted the new session and
|
||||
* destroyed the old repository handle — in that order, which is the part that
|
||||
* matters. What is left is the half only the frontend owns: a `RepositoryClient`
|
||||
* bound to the new session, and the player's reporting configuration.
|
||||
*
|
||||
* Deliberately *not* a login: no password is involved and no token is minted,
|
||||
* because switching must leave both profiles able to come back with one tap.
|
||||
*
|
||||
* TRACES: UR-082 | DR-270
|
||||
*/
|
||||
async function adoptSwitchedSession() {
|
||||
const session = await commands.authGetSession();
|
||||
if (!session) throw new Error("No session after profile switch");
|
||||
|
||||
if (repository) {
|
||||
try {
|
||||
await repository.destroy();
|
||||
} catch (error) {
|
||||
log.error("Failed to destroy repository during switch:", error);
|
||||
}
|
||||
}
|
||||
|
||||
repository = new RepositoryClient();
|
||||
await repository.create(
|
||||
session.serverUrl,
|
||||
session.userId,
|
||||
session.accessToken,
|
||||
session.serverId,
|
||||
);
|
||||
|
||||
try {
|
||||
const deviceId = await getDeviceId();
|
||||
await commands.playerConfigureJellyfin(
|
||||
session.serverUrl,
|
||||
session.accessToken,
|
||||
session.userId,
|
||||
deviceId,
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Failed to reconfigure player after switch:", error);
|
||||
}
|
||||
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: { id: session.userId, name: session.username } as User,
|
||||
serverUrl: session.serverUrl,
|
||||
serverName: session.serverName,
|
||||
error: null,
|
||||
securityWarning: null,
|
||||
needsReauth: false,
|
||||
isVerifying: false,
|
||||
sessionVerified: session.verified,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
initialize,
|
||||
@@ -620,6 +680,7 @@ function createAuthStore() {
|
||||
getUserId,
|
||||
getServerUrl,
|
||||
retryVerification,
|
||||
adoptSwitchedSession,
|
||||
cleanupEventListeners,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user