Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
cb1a23c
chore: Move DerivedTokenGenerator to the Auth module
LouisCAD Jul 7, 2026
6bf4b7b
feat: Introduce the RestoreFromBackupManager interface
LouisCAD Jul 7, 2026
a33e9dc
chore: Add suspending allUsers() in UserDao
LouisCAD Jul 8, 2026
32eccb9
feat: Add WIP RestoreFromBackupManagerImpl
LouisCAD Jul 8, 2026
ac89757
chore: Forward issues from DerivedTokenGenerator in RestoreFromBackup…
LouisCAD Jul 9, 2026
1ef83db
chore: Store user token to device association in the db instead of a …
LouisCAD Jul 9, 2026
8f773d4
chore: Move ensureRestorationIsHandled from impl to the sealed class
LouisCAD Jul 21, 2026
811938a
chore: Add `shouldShowRestorationScreen` helper in RestoreFromBackupM…
LouisCAD Jul 21, 2026
b7d365c
feat: Auto retry post-restoration token derivation if appropriate
LouisCAD Jul 21, 2026
7d7e175
fix: Use a getter to get the up-to-date userDataCleanableList
LouisCAD Jul 21, 2026
379fb71
feat: Remove relevant users when the user gives up backup restoration
LouisCAD Jul 21, 2026
578056c
docs: Update CrossAppLogin README.md
LouisCAD Jul 21, 2026
30e3236
fix: Fix crashes and race condition
LouisCAD Jul 21, 2026
9cb8e27
feat: Add barebones RestoringFromBackupFailedScreen
LouisCAD Jul 21, 2026
d2977e9
feat: Move landscape UI of backup restoration failed to the end
LouisCAD Jul 29, 2026
fe6832e
fix: Fix AccountUtils tests
LouisCAD Jul 29, 2026
f476bba
fix: Ensure attestation tokens cannot be reused
LouisCAD Jul 30, 2026
7aff9d8
feat: Add strings
Elouan1411 Jul 31, 2026
a288a06
fix: Update UI
Elouan1411 Jul 31, 2026
5153c98
chore: Extract nested when
LouisCAD Aug 3, 2026
d981a32
fix: Don't auto-retry if we received http 401
LouisCAD Aug 3, 2026
4aa1948
chore: Add braces to if branch
LouisCAD Aug 3, 2026
8f4b1db
chore: Add fast-path for not set up app
LouisCAD Aug 3, 2026
fcac944
feat: Support the Authenticator app
LouisCAD Aug 11, 2026
102cbca
chore: Fix compilation by adding a default parameter value
LouisCAD Aug 11, 2026
678b700
chore: Ensure we don't auto-retry http 400 requests
LouisCAD Aug 12, 2026
8203de8
docs: Add KDoc to explain why a parameter is needed
LouisCAD Aug 17, 2026
6784b85
refactor: Extract 2 functions
LouisCAD Aug 17, 2026
46c9e4d
chore: Use our Margin constants
LouisCAD Aug 17, 2026
efabd6e
chore: Rename composable to be more accurate
LouisCAD Aug 17, 2026
b8c0830
feat: Add WIP RestoreFromBackupManagerImpl
LouisCAD Jul 8, 2026
b31717b
chore: Store user token to device association in the db instead of a …
LouisCAD Jul 9, 2026
fc18ac1
test: Add RestoreFromBackupManagerImpl tests for data transfer support
Copilot Jul 30, 2026
adeab26
test: Allow DerivedTokenGenerator to be subclassed from tests
LouisCAD Aug 18, 2026
1ea07df
test: Add timeout for tests so they fail sooner
LouisCAD Aug 18, 2026
5cc7d3c
test: Use kotest assertions for better readability
LouisCAD Aug 18, 2026
21c16f8
test: Move helpers at the end of the file
LouisCAD Aug 18, 2026
e9dea3b
test: Use new testUserDb helper for conciseness
LouisCAD Aug 18, 2026
75e500a
test: Replace reference comment by constant
LouisCAD Aug 18, 2026
da5b142
test: Add extra test helper for more conciseness
LouisCAD Aug 18, 2026
85ac25d
test: Add custom test scope for more conciseness
LouisCAD Aug 18, 2026
1db50ce
test: Add extra assertions in RestoreFromBackupManagerImplTest
LouisCAD Aug 18, 2026
8086fe8
test: Update function names to be more readable
LouisCAD Aug 18, 2026
80eca9e
chore(CrossAppLogin): Avoid disabling cross app login in more cases
LouisCAD Aug 11, 2026
2a3cb1e
feat: Add BackupAgent helpers
LouisCAD Aug 24, 2026
47bd369
feat: Update TransportAwareFileBackupHandler for more granularity
LouisCAD Aug 25, 2026
8327fad
refactor: Extract BackupPolicy to separate file
LouisCAD Aug 31, 2026
b960fc8
chore: Remove BackupAgentBase and TransportAwareFileBackupHandler
LouisCAD Aug 31, 2026
692f3a3
feat: Introduce FullBackupAgent
LouisCAD Aug 31, 2026
0380a29
feat: Add FullBackup to restore file not included in XML rules
LouisCAD Sep 1, 2026
f179c79
refactor: Rename .java to .kt
LouisCAD Sep 1, 2026
9f6c202
fix: Fix compilation by converting FullBackup.java to Kotlin
LouisCAD Sep 1, 2026
a720335
chore: Replace unneeded userDb property with constructor property
LouisCAD Sep 3, 2026
7b3c22d
chore: Move companion object at the end of the file
LouisCAD Sep 3, 2026
10034de
chore: Make BaseAccountUtilsTest companion object protected
LouisCAD Sep 3, 2026
07d3b8f
docs: Add KDoc and remove unneeded nullability
LouisCAD Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ dependencies {
implementation(project(":AppIntegrity"))
implementation(project(":Network"))
implementation(project(":Sentry"))
implementation(project(":Ui:Compose:Margin"))

implementation(platform(core.compose.bom))
implementation(core.compose.ui)
implementation(core.compose.runtime)
implementation(core.compose.ui)
implementation(core.compose.material3)
implementation(core.activity.compose)
implementation(core.compose.ui.tooling.preview)
debugImplementation(core.compose.ui.tooling)

implementation(core.appcompat)
implementation(core.androidx.core.ktx)
Expand All @@ -64,5 +69,6 @@ dependencies {
testImplementation(core.junit)
testImplementation(core.androidx.test.core)
testImplementation(core.kotlinx.coroutines.test)
testImplementation(core.kotest.assertions)
testImplementation(core.robolectric)
}
213 changes: 213 additions & 0 deletions Auth/schemas/com.infomaniak.core.auth.room.UserDatabase/10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6d4be66bdbd5459afb46f129e14738f7",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `firstname` TEXT NOT NULL, `lastname` TEXT NOT NULL, `email` TEXT NOT NULL, `avatar` TEXT, `phones` TEXT, `card` TEXT, `login` TEXT NOT NULL, `isStaff` INTEGER NOT NULL DEFAULT false, `organizations` TEXT NOT NULL, `preferences_security_score` INTEGER DEFAULT 0, `preferences_security_dateLastChangedPassword` INTEGER DEFAULT 0, `preferences_organizationPreference_currentOrganizationId` INTEGER NOT NULL DEFAULT 0, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `tokenType` TEXT NOT NULL, `expiresIn` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `scope` TEXT, `expiresAt` INTEGER, `isTemporary` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "firstname",
"columnName": "firstname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastname",
"columnName": "lastname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT"
},
{
"fieldPath": "phones",
"columnName": "phones",
"affinity": "TEXT"
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT"
},
{
"fieldPath": "login",
"columnName": "login",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isStaff",
"columnName": "isStaff",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "organizations",
"columnName": "organizations",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "preferences.security.score",
"columnName": "preferences_security_score",
"affinity": "INTEGER",
"defaultValue": "0"
},
{
"fieldPath": "preferences.security.dateLastChangedPassword",
"columnName": "preferences_security_dateLastChangedPassword",
"affinity": "INTEGER",
"defaultValue": "0"
},
{
"fieldPath": "preferences.organizationPreference.currentOrganizationId",
"columnName": "preferences_organizationPreference_currentOrganizationId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "apiToken.accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiToken.refreshToken",
"columnName": "refreshToken",
"affinity": "TEXT"
},
{
"fieldPath": "apiToken.tokenType",
"columnName": "tokenType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiToken.expiresIn",
"columnName": "expiresIn",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "apiToken.userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "apiToken.scope",
"columnName": "scope",
"affinity": "TEXT"
},
{
"fieldPath": "apiToken.expiresAt",
"columnName": "expiresAt",
"affinity": "INTEGER"
},
{
"fieldPath": "apiToken.isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "CurrentUserId",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `uniqueKey` TEXT NOT NULL, PRIMARY KEY(`uniqueKey`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
},
{
"fieldPath": "uniqueKey",
"columnName": "uniqueKey",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uniqueKey"
]
}
},
{
"tableName": "TokenDeviceBinding",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `androidId` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `User`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "androidId",
"columnName": "androidId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId"
]
},
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d4be66bdbd5459afb46f129e14738f7')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,4 @@
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd2186f80b68b770c4f4081c454b43377')"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.annotation.CallSuper
import androidx.room.withTransaction
import com.infomaniak.core.auth.backup.RestoreFromBackupManager
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.common.AssociatedUserDataCleanable
Expand All @@ -38,9 +39,10 @@ import kotlinx.coroutines.flow.flowOf
*/
abstract class AbstractCurrentUserAccountUtils(
appContext: Context,
userDataCleanableList: List<AssociatedUserDataCleanable> = emptyList(),
userDataCleanableList: () -> List<AssociatedUserDataCleanable> = { emptyList() },
Comment thread
LouisCAD marked this conversation as resolved.
userDatabase: UserDatabase = UserDatabase.instantiateDataBase(appContext),
) : UserAccountUtils(appContext, userDataCleanableList, userDatabase) {
restoreFromBackupManager: RestoreFromBackupManager = RestoreFromBackupManager.instance,
) : UserAccountUtils(appContext, userDataCleanableList, userDatabase, restoreFromBackupManager) {

/**
* If you need a live [User] instead of just its id, use [currentUserFlow]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import androidx.collection.ArrayMap
import com.infomaniak.core.auth.models.user.Card
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.network.networking.HttpClientConfig
import com.infomaniak.core.login.ApiToken
import com.infomaniak.core.network.networking.HttpClientConfig
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Cache
Expand Down Expand Up @@ -125,7 +125,7 @@ abstract class BaseCredentialManager : UserExistenceChecker {

private suspend fun getDefaultTokenInterceptorListener(userId: Int): TokenInterceptorListener {
var user = userDatabase.userDao().findById(userId)
return object : TokenInterceptorListener {
return object : TokenInterceptorListener(dedicatedUserId = userId) {
override suspend fun onRefreshTokenSuccess(apiToken: ApiToken) {
setUserToken(user, apiToken)
}
Expand Down
13 changes: 11 additions & 2 deletions Auth/src/main/kotlin/com/infomaniak/core/auth/CredentialManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package com.infomaniak.core.auth

import androidx.lifecycle.LiveData
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.common.AssociatedUserDataCleanable
import com.infomaniak.core.login.ApiToken
import splitties.init.appCtx

/**
* CredentialManager: Adds a currentUserId and currentUser management layer to [BaseCredentialManager]
Expand All @@ -29,12 +32,18 @@ import com.infomaniak.core.login.ApiToken
* blocking methods which is fixed in the alternative classes.
*/
@Deprecated("It's recommended to use UserAccountUtils, AbstractCurrentUserAccountUtils or PersistedCurrentUserAccountUtils")
abstract class CredentialManager : BaseCredentialManager() {
abstract class CredentialManager(
userDataCleanableList: () -> List<AssociatedUserDataCleanable>
) : UserAccountUtils(
Comment thread
LouisCAD marked this conversation as resolved.
appContext = appCtx,
userDataCleanableList = userDataCleanableList,
userDatabase = UserDatabase.instance,
) {

abstract val currentUserId: Int
abstract var currentUser: User?

fun getAllUsers(): LiveData<List<User>> = userDatabase.userDao().getAll()
fun getAllUsers(): LiveData<List<User>> = userDatabase.userDao().allAsLiveData()

suspend fun getAllUsersCount(): Int = userDatabase.userDao().userCount()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2026 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.auth

import com.infomaniak.core.appintegrity.AppIntegrityIssue
import com.infomaniak.core.auth.DerivedTokenGenerator.Issue

fun Issue.shouldReport(): Boolean = when (this) {
is Issue.AppIntegrityCheckFailed -> false
is Issue.ErrorResponse -> response.code !in 500..599
is Issue.NetworkIssue -> false
is Issue.OtherIssue -> true
}

fun Issue.shouldRetryAutomatically(): Boolean = when (this) {
is Issue.AppIntegrityCheckFailed -> shouldRetryAutomatically()
is Issue.ErrorResponse -> shouldRetryAutomatically()
is Issue.NetworkIssue -> true
is Issue.OtherIssue -> false
}

private fun Issue.AppIntegrityCheckFailed.shouldRetryAutomatically(): Boolean = when (details.issue) {
is AppIntegrityIssue.RetryLater, is AppIntegrityIssue.Internal -> true
is AppIntegrityIssue.DeviceIssue, is AppIntegrityIssue.DevError, is AppIntegrityIssue.SuspiciousError -> false
}

private fun Issue.ErrorResponse.shouldRetryAutomatically(): Boolean = when (response.code) {
400, 401 -> false
else -> true
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.crossapplogin.back
package com.infomaniak.core.auth

import com.infomaniak.core.appintegrity.exceptions.AppIntegrityException
import com.infomaniak.core.common.Xor
import com.infomaniak.core.login.ApiToken
import okhttp3.Response

internal sealed interface DerivedTokenGenerator {
interface DerivedTokenGenerator {

suspend fun attemptDerivingOneOfTheseTokens(tokensToTry: Set<String>): Xor<ApiToken, Issue>
suspend fun isAppIntegrityGuaranteedToFail(): Boolean
Expand Down
Loading
Loading