Add setting to protect a user profile with a PIN (#356)

Allows for setting a per-user profile PIN that must be entered to switch
to the user. The PIN is a sequence of directional buttons, ie D-Pad
left/right/up/down.

This PIN is per device per user per server. The PIN is stored locally
and not sent nor shared with the server. If you forget your PIN, can
still access the app using server credentials which also removes the
PIN.

Additionally, there is another setting to disable automatically sign in.
When enabled (default) the last user is automatically restored, even if
there is a PIN. If disabled, a server & user must always be selected
when opening the app.

Closes #268
Closes #321
This commit is contained in:
damontecres 2025-12-04 14:37:27 -05:00 committed by GitHub
parent a294661520
commit 8ea84b3efe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 845 additions and 30 deletions

View file

@ -23,7 +23,7 @@ import java.util.UUID
@Database(
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
version = 9,
version = 10,
exportSchema = true,
autoMigrations = [
AutoMigration(3, 4),
@ -32,6 +32,7 @@ import java.util.UUID
AutoMigration(6, 7),
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
],
)
@TypeConverters(Converters::class)

View file

@ -27,7 +27,7 @@ interface JellyfinServerDao {
}
}
@Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun addUser(user: JellyfinUser): Long
@Update

View file

@ -132,6 +132,10 @@ class ServerRepository
return false
}
fun closeSession() {
_current.value = null
}
/**
* Given a successful [AuthenticationResult], switch to the user that just authenticated
*/
@ -225,6 +229,21 @@ class ServerRepository
}
}
suspend fun setUserPin(
user: JellyfinUser,
pin: String?,
) = withContext(Dispatchers.IO) {
val newUser = user.copy(pin = pin)
val updatedUser = serverDao.addOrUpdateUser(newUser)
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
// Updating current user, so push out the change
current.value?.let {
val newCurrent = it.copy(user = updatedUser)
_current.setValueOnMain(newCurrent)
}
}
}
companion object {
fun getServerSharedPreferences(context: Context): SharedPreferences =
context.getSharedPreferences(

View file

@ -50,9 +50,10 @@ data class JellyfinUser(
@ColumnInfo(index = true)
val serverId: UUID,
val accessToken: String?,
val pin: String? = null,
) {
override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken=${accessToken.isNotNullOrBlank()})"
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})"
}
data class JellyfinServerUsers(