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:
@@ -30,6 +30,39 @@ pub async fn auth_initialize(
|
||||
// Try to restore session from storage
|
||||
log::info!("[AuthManager] Restoring session from storage...");
|
||||
|
||||
// A PIN-protected profile is not restored automatically. Restoring it would
|
||||
// hand the app a working token before anybody entered the code, leaving the
|
||||
// picker as decoration over a session that was already live — the gate has
|
||||
// to be on the session itself, not on which screen is shown. The frontend
|
||||
// sees `None`, asks `profiles_startup_target`, and lands on the picker.
|
||||
//
|
||||
// TRACES: UR-083 | DR-268, DR-274
|
||||
{
|
||||
let db_service = {
|
||||
let db = database.0.lock().map_err(|e| e.to_string())?;
|
||||
std::sync::Arc::new(db.service())
|
||||
};
|
||||
let locked: Option<String> = crate::storage::db_service::DatabaseService::query_optional(
|
||||
&*db_service,
|
||||
crate::storage::db_service::Query::new(
|
||||
"SELECT u.id FROM users u
|
||||
JOIN user_pins p ON p.user_id = u.id
|
||||
WHERE u.is_active = 1",
|
||||
),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some(user_id) = locked {
|
||||
log::info!(
|
||||
"[AuthManager] Active profile {} is PIN-protected; not restoring its session",
|
||||
user_id
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the existing storage_get_active_session function
|
||||
let active_session =
|
||||
match crate::commands::storage::storage_get_active_session(database, credentials).await {
|
||||
|
||||
Reference in New Issue
Block a user