From 9bed1db76d654a2a0d234c6c76b84e98fa6803a2 Mon Sep 17 00:00:00 2001 From: Neil Marietta Date: Thu, 10 Oct 2024 22:20:26 +0200 Subject: [PATCH] feat!(auth): Added Global SSO Change Password support. MIGRATIONS: - UserDatabase.MIGRATION_6 - AccountDatabase.MIGRATION_9 --- .../57.json | 3508 +++++++++++++++++ .../data/db/AccountManagerDatabase.kt | 5 +- .../db/AccountManagerDatabaseMigrations.kt | 7 + .../viewmodel/AccountSettingsViewModelTest.kt | 1 + .../usecase/ObserveUserRecoveryStateTest.kt | 3 +- .../core/account/data/db/AccountDatabase.kt | 12 + .../api/response/SRPAuthenticationResponse.kt | 2 +- .../me/proton/core/auth/domain/LogTag.kt | 1 + .../usecase/PostLoginLessAccountSetup.kt | 1 + .../domain/usecase/ValidateServerProof.kt | 2 +- .../usecase/sso/ChangeBackupPassword.kt | 82 + .../domain/usecase/sso/CheckOtherDevices.kt | 2 +- .../domain/usecase/sso/GetEncryptedSecret.kt | 2 +- .../domain/usecase/AccountAvailabilityTest.kt | 3 +- .../usecase/PostLoginSsoAccountSetupTest.kt | 4 +- .../compose/DeviceSecretScreen.kt | 14 +- .../sso/BackupPasswordChangeOperation.kt | 29 + .../compose/sso/BackupPasswordChangeScreen.kt | 221 ++ .../compose/sso/BackupPasswordChangeState.kt | 40 + .../sso/BackupPasswordChangeViewModel.kt | 108 + .../sso/BackupPasswordInputViewModel.kt | 6 - .../compose/sso/BackupPasswordSetupScreen.kt | 8 +- .../compose/sso/BackupPasswordSetupState.kt | 13 +- .../sso/BackupPasswordSetupViewModel.kt | 4 +- .../compose/sso/ConfirmationDigits.kt | 8 +- .../compose/sso/MemberApprovalScreen.kt | 6 +- .../compose/sso/PasswordFormError.kt | 6 + .../compose/sso/RequestAdminHelpViewModel.kt | 8 +- .../presentation/compose/sso/ThrowableExt.kt | 11 + .../compose/sso/WaitingAdminScreen.kt | 3 +- .../src/main/res/values/strings.xml | 5 + .../sso/BackupPasswordChangeScreenTest.kt | 66 + .../sso/BackupPasswordSetupScreenTest.kt | 4 +- .../sso/BackupPasswordSetupViewModelTest.kt | 4 +- ...upPasswordChangeScreenTest_idle screen.png | 3 + ...ordChangeScreenTest_password too short.png | 3 + ...hangeScreenTest_passwords do not match.png | 3 + .../ChooseExternalEmailViewModelTest.kt | 1 + .../viewmodel/signup/SignupViewModelTest.kt | 1 + .../proton/core/contact/tests/SampleData.kt | 1 + .../57.json | 3508 +++++++++++++++++ .../core/coreexample/db/AppDatabase.kt | 5 +- .../coreexample/db/AppDatabaseMigrations.kt | 7 + .../core/data/room/db/CommonConverters.kt | 8 + .../UpdateKeysForPasswordChangeRequest.kt | 14 +- .../key/data/api/response/UserResponse.kt | 2 + .../repository/PrivateKeyRepositoryImpl.kt | 16 +- .../domain/repository/PrivateKeyRepository.kt | 11 +- .../proton/core/contact/tests/SampleData.kt | 1 + .../data/repository/testing/test_entities.kt | 1 + .../viewmodel/UpgradePlansViewModelTest.kt | 1 + .../core/push/data/testing/test_entities.kt | 1 + .../usecase/PerformUpdateUserPassword.kt | 3 +- .../usecase/PerformResetUserPasswordTest.kt | 3 +- .../usecase/PerformUpdateLoginPasswordTest.kt | 3 +- .../usecase/PerformUpdateUserPasswordTest.kt | 9 +- .../ui/PasswordManagementFragment.kt | 1 + .../viewmodel/PasswordManagementViewModel.kt | 20 +- .../src/main/res/values/strings.xml | 1 + .../PasswordManagementViewModelTest.kt | 5 + .../me/proton/core/user/data/TestUsers.kt | 2 + .../user/data/UserManagerPasswordTests.kt | 3 +- .../repository/UserRepositoryImplTests.kt | 1 + .../proton/core/user/data/UserManagerImpl.kt | 11 +- .../proton/core/user/data/db/UserDatabase.kt | 14 + .../core/user/data/entity/UserEntity.kt | 1 + .../core/user/data/extension/UserMapper.kt | 3 + .../core/user/data/UserManagerImplTest.kt | 4 + .../me/proton/core/user/domain/UserManager.kt | 7 +- .../me/proton/core/user/domain/entity/User.kt | 1 + .../proton/core/user/domain/extension/User.kt | 8 +- .../core/user/domain/extension/UserKtTest.kt | 1 + .../core/util/kotlin/SerializationUtils.kt | 10 + 73 files changed, 7817 insertions(+), 80 deletions(-) create mode 100644 account-manager/data-db/schemas/me.proton.core.accountmanager.data.db.AccountManagerDatabase/57.json create mode 100644 auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/ChangeBackupPassword.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeOperation.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreen.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeState.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeViewModel.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/PasswordFormError.kt create mode 100644 auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ThrowableExt.kt create mode 100644 auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreenTest.kt create mode 100644 auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_idle screen.png create mode 100644 auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_password too short.png create mode 100644 auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_passwords do not match.png create mode 100644 coreexample/schemas/me.proton.android.core.coreexample.db.AppDatabase/57.json diff --git a/account-manager/data-db/schemas/me.proton.core.accountmanager.data.db.AccountManagerDatabase/57.json b/account-manager/data-db/schemas/me.proton.core.accountmanager.data.db.AccountManagerDatabase/57.json new file mode 100644 index 000000000..0a9590759 --- /dev/null +++ b/account-manager/data-db/schemas/me.proton.core.accountmanager.data.db.AccountManagerDatabase/57.json @@ -0,0 +1,3508 @@ +{ + "formatVersion": 1, + "database": { + "version": 57, + "identityHash": "f040ffee30acc3a49d2e630101c22559", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "product" + ] + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sessionId" + ] + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, `fido2AuthenticationOptionsJson` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fido2AuthenticationOptionsJson", + "columnName": "fido2AuthenticationOptionsJson", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sessionId" + ] + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `createdAtUtc` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `type` INTEGER, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, `flags` TEXT, `maxBaseSpace` INTEGER, `maxDriveSpace` INTEGER, `usedBaseSpace` INTEGER, `usedDriveSpace` INTEGER, `recovery_state` INTEGER, `recovery_startTime` INTEGER, `recovery_endTime` INTEGER, `recovery_sessionId` TEXT, `recovery_reason` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtUtc", + "columnName": "createdAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isPrivate", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxBaseSpace", + "columnName": "maxBaseSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxDriveSpace", + "columnName": "maxDriveSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedBaseSpace", + "columnName": "usedBaseSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDriveSpace", + "columnName": "usedDriveSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.state", + "columnName": "recovery_state", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.startTime", + "columnName": "recovery_startTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.endTime", + "columnName": "recovery_endTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.sessionId", + "columnName": "recovery_sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recovery.reason", + "columnName": "recovery_reason", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, `recoverySecretHash` TEXT, `recoverySecretSignature` TEXT, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recoverySecretHash", + "columnName": "recoverySecretHash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recoverySecretSignature", + "columnName": "recoverySecretSignature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "keyId" + ] + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "addressId" + ] + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "keyId" + ] + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "keyId" + ] + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `ignoreKT` INTEGER, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignoreKT", + "columnName": "ignoreKT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email" + ] + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email", + "publicKey" + ] + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "PublicAddressInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `warnings` TEXT NOT NULL, `protonMx` INTEGER NOT NULL, `isProton` INTEGER NOT NULL, `addressSignedKeyList_data` TEXT, `addressSignedKeyList_signature` TEXT, `addressSignedKeyList_minEpochId` INTEGER, `addressSignedKeyList_maxEpochId` INTEGER, `addressSignedKeyList_expectedMinEpochId` INTEGER, `catchAllSignedKeyList_data` TEXT, `catchAllSignedKeyList_signature` TEXT, `catchAllSignedKeyList_minEpochId` INTEGER, `catchAllSignedKeyList_maxEpochId` INTEGER, `catchAllSignedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "warnings", + "columnName": "warnings", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protonMx", + "columnName": "protonMx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isProton", + "columnName": "isProton", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addressSignedKeyList.data", + "columnName": "addressSignedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.signature", + "columnName": "addressSignedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.minEpochId", + "columnName": "addressSignedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.maxEpochId", + "columnName": "addressSignedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.expectedMinEpochId", + "columnName": "addressSignedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.data", + "columnName": "catchAllSignedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.signature", + "columnName": "catchAllSignedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.minEpochId", + "columnName": "catchAllSignedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.maxEpochId", + "columnName": "catchAllSignedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.expectedMinEpochId", + "columnName": "catchAllSignedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email" + ] + }, + "indices": [ + { + "name": "index_PublicAddressInfoEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressInfoEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyDataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `emailAddressType` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `source` INTEGER, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressInfoEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emailAddressType", + "columnName": "emailAddressType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email", + "publicKey" + ] + }, + "indices": [ + { + "name": "index_PublicAddressKeyDataEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyDataEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationToken", + "columnName": "verificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "clientId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MailSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `autoSaveContacts` INTEGER, `composerMode` INTEGER, `messageButtons` INTEGER, `showImages` INTEGER, `showMoved` INTEGER, `viewMode` INTEGER, `viewLayout` INTEGER, `swipeLeft` INTEGER, `swipeRight` INTEGER, `shortcuts` INTEGER, `pmSignature` INTEGER, `numMessagePerPage` INTEGER, `autoDeleteSpamAndTrashDays` INTEGER, `draftMimeType` TEXT, `receiveMimeType` TEXT, `showMimeType` TEXT, `enableFolderColor` INTEGER, `inheritParentFolderColor` INTEGER, `rightToLeft` INTEGER, `attachPublicKey` INTEGER, `sign` INTEGER, `pgpScheme` INTEGER, `promptPin` INTEGER, `stickyLabels` INTEGER, `confirmLink` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "autoSaveContacts", + "columnName": "autoSaveContacts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "composerMode", + "columnName": "composerMode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageButtons", + "columnName": "messageButtons", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "showImages", + "columnName": "showImages", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "showMoved", + "columnName": "showMoved", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "viewMode", + "columnName": "viewMode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "viewLayout", + "columnName": "viewLayout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipeLeft", + "columnName": "swipeLeft", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipeRight", + "columnName": "swipeRight", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shortcuts", + "columnName": "shortcuts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pmSignature", + "columnName": "pmSignature", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numMessagePerPage", + "columnName": "numMessagePerPage", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "autoDeleteSpamAndTrashDays", + "columnName": "autoDeleteSpamAndTrashDays", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "draftMimeType", + "columnName": "draftMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "receiveMimeType", + "columnName": "receiveMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showMimeType", + "columnName": "showMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableFolderColor", + "columnName": "enableFolderColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "inheritParentFolderColor", + "columnName": "inheritParentFolderColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rightToLeft", + "columnName": "rightToLeft", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachPublicKey", + "columnName": "attachPublicKey", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign", + "columnName": "sign", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pgpScheme", + "columnName": "pgpScheme", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "promptPin", + "columnName": "promptPin", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "stickyLabels", + "columnName": "stickyLabels", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "confirmLink", + "columnName": "confirmLink", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `density` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `earlyAccess` INTEGER, `deviceRecovery` INTEGER, `telemetry` INTEGER, `crashReports` INTEGER, `sessionAccountRecovery` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_registeredKeys` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deviceRecovery", + "columnName": "deviceRecovery", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "telemetry", + "columnName": "telemetry", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "crashReports", + "columnName": "crashReports", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sessionAccountRecovery", + "columnName": "sessionAccountRecovery", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.registeredKeys", + "columnName": "twoFA_registeredKeys", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxCalendars", + "columnName": "maxCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedCalendars", + "columnName": "usedCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ContactEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `contactId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`contactId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactId" + ] + }, + "indices": [ + { + "name": "index_ContactEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ContactCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contactId` TEXT NOT NULL, `type` INTEGER NOT NULL, `data` TEXT NOT NULL, `signature` TEXT, `cardId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`contactId`) REFERENCES `ContactEntity`(`contactId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cardId", + "columnName": "cardId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "cardId" + ] + }, + "indices": [ + { + "name": "index_ContactCardEntity_contactId", + "unique": false, + "columnNames": [ + "contactId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactCardEntity_contactId` ON `${TABLE_NAME}` (`contactId`)" + } + ], + "foreignKeys": [ + { + "table": "ContactEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactId" + ], + "referencedColumns": [ + "contactId" + ] + } + ] + }, + { + "tableName": "ContactEmailEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `contactEmailId` TEXT NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `defaults` INTEGER NOT NULL, `order` INTEGER NOT NULL, `contactId` TEXT NOT NULL, `canonicalEmail` TEXT, `isProton` INTEGER, `lastUsedTime` INTEGER NOT NULL, PRIMARY KEY(`contactEmailId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contactId`) REFERENCES `ContactEntity`(`contactId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactEmailId", + "columnName": "contactEmailId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaults", + "columnName": "defaults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "canonicalEmail", + "columnName": "canonicalEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isProton", + "columnName": "isProton", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastUsedTime", + "columnName": "lastUsedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactEmailId" + ] + }, + "indices": [ + { + "name": "index_ContactEmailEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEmailEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_ContactEmailEntity_contactId", + "unique": false, + "columnNames": [ + "contactId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEmailEntity_contactId` ON `${TABLE_NAME}` (`contactId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ContactEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactId" + ], + "referencedColumns": [ + "contactId" + ] + } + ] + }, + { + "tableName": "ContactEmailLabelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contactEmailId` TEXT NOT NULL, `labelId` TEXT NOT NULL, PRIMARY KEY(`contactEmailId`, `labelId`), FOREIGN KEY(`contactEmailId`) REFERENCES `ContactEmailEntity`(`contactEmailId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "contactEmailId", + "columnName": "contactEmailId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "labelId", + "columnName": "labelId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactEmailId", + "labelId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ContactEmailEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactEmailId" + ], + "referencedColumns": [ + "contactEmailId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, `fetchedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fetchedAt", + "columnName": "fetchedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "config" + ] + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LabelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `labelId` TEXT NOT NULL, `parentId` TEXT, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `path` TEXT NOT NULL, `color` TEXT NOT NULL, `order` INTEGER NOT NULL, `isNotified` INTEGER, `isExpanded` INTEGER, `isSticky` INTEGER, PRIMARY KEY(`userId`, `labelId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "labelId", + "columnName": "labelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "isNotified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isExpanded", + "columnName": "isExpanded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSticky", + "columnName": "isSticky", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "labelId" + ] + }, + "indices": [ + { + "name": "index_LabelEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_LabelEntity_labelId", + "unique": false, + "columnNames": [ + "labelId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_labelId` ON `${TABLE_NAME}` (`labelId`)" + }, + { + "name": "index_LabelEntity_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_parentId` ON `${TABLE_NAME}` (`parentId`)" + }, + { + "name": "index_LabelEntity_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_LabelEntity_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_type` ON `${TABLE_NAME}` (`type`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "FeatureFlagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "featureId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "featureId" + ] + }, + "indices": [ + { + "name": "index_FeatureFlagEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_FeatureFlagEntity_featureId", + "unique": false, + "columnNames": [ + "featureId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ChallengeFrameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))", + "fields": [ + { + "fieldPath": "challengeFrame", + "columnName": "challengeFrame", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flow", + "columnName": "flow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "focusTime", + "columnName": "focusTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clicks", + "columnName": "clicks", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "copy", + "columnName": "copy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paste", + "columnName": "paste", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keys", + "columnName": "keys", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "challengeFrame" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PushEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `pushId` TEXT NOT NULL, `objectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `pushId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushId", + "columnName": "pushId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "pushId" + ] + }, + "indices": [ + { + "name": "index_PushEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PushEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_PushEntity_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PushEntity_type` ON `${TABLE_NAME}` (`type`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "PurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `planName` TEXT NOT NULL, `planCycle` INTEGER NOT NULL, `purchaseState` TEXT NOT NULL, `purchaseFailure` TEXT, `paymentProvider` TEXT NOT NULL, `paymentOrderId` TEXT, `paymentToken` TEXT, `paymentCurrency` TEXT NOT NULL, `paymentAmount` INTEGER NOT NULL, PRIMARY KEY(`planName`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "planCycle", + "columnName": "planCycle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseState", + "columnName": "purchaseState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "purchaseFailure", + "columnName": "purchaseFailure", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentProvider", + "columnName": "paymentProvider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentOrderId", + "columnName": "paymentOrderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentCurrency", + "columnName": "paymentCurrency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentAmount", + "columnName": "paymentAmount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "planName" + ] + }, + "indices": [ + { + "name": "index_PurchaseEntity_planName", + "unique": false, + "columnNames": [ + "planName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_planName` ON `${TABLE_NAME}` (`planName`)" + }, + { + "name": "index_PurchaseEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_PurchaseEntity_purchaseState", + "unique": false, + "columnNames": [ + "purchaseState" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_purchaseState` ON `${TABLE_NAME}` (`purchaseState`)" + }, + { + "name": "index_PurchaseEntity_paymentProvider", + "unique": false, + "columnNames": [ + "paymentProvider" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_paymentProvider` ON `${TABLE_NAME}` (`paymentProvider`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "GooglePurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))", + "fields": [ + { + "fieldPath": "googlePurchaseToken", + "columnName": "googlePurchaseToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "googlePurchaseToken" + ] + }, + "indices": [ + { + "name": "index_GooglePurchaseEntity_paymentToken", + "unique": true, + "columnNames": [ + "paymentToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ObservabilityEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TelemetryEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `group` TEXT NOT NULL, `name` TEXT NOT NULL, `values` TEXT NOT NULL, `dimensions` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dimensions", + "columnName": "dimensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TelemetryEventEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TelemetryEventEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `changeId` TEXT NOT NULL, `counterEncrypted` TEXT NOT NULL, `emailEncrypted` TEXT NOT NULL, `epochIdEncrypted` TEXT NOT NULL, `creationTimestampEncrypted` TEXT NOT NULL, `publicKeysEncrypted` TEXT NOT NULL, `isObsolete` TEXT NOT NULL, PRIMARY KEY(`userId`, `changeId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "changeId", + "columnName": "changeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "counterEncrypted", + "columnName": "counterEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emailEncrypted", + "columnName": "emailEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "epochIdEncrypted", + "columnName": "epochIdEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTimestampEncrypted", + "columnName": "creationTimestampEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKeysEncrypted", + "columnName": "publicKeysEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isObsolete", + "columnName": "isObsolete", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "changeId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SelfAuditResultEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "NotificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, `userId` TEXT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL, `payload` TEXT NOT NULL, PRIMARY KEY(`userId`, `notificationId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payload", + "columnName": "payload", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_NotificationEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_NotificationEntity_notificationId", + "unique": false, + "columnNames": [ + "notificationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_notificationId` ON `${TABLE_NAME}` (`notificationId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "RecoveryFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `createdAtUtcMillis` INTEGER NOT NULL, `keyCount` INTEGER, `recoveryFile` TEXT NOT NULL, `recoverySecretHash` TEXT NOT NULL, PRIMARY KEY(`recoverySecretHash`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAtUtcMillis", + "columnName": "createdAtUtcMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyCount", + "columnName": "keyCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recoveryFile", + "columnName": "recoveryFile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recoverySecretHash", + "columnName": "recoverySecretHash", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recoverySecretHash" + ] + }, + "indices": [ + { + "name": "index_RecoveryFileEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_RecoveryFileEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DeviceSecretEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `secret` TEXT NOT NULL, `token` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_DeviceSecretEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DeviceSecretEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AuthDeviceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `addressId` TEXT, `state` INTEGER NOT NULL, `name` TEXT NOT NULL, `localizedClientName` TEXT NOT NULL, `platform` TEXT, `createdAtUtcSeconds` INTEGER NOT NULL, `activatedAtUtcSeconds` INTEGER, `rejectedAtUtcSeconds` INTEGER, `activationToken` TEXT, `lastActivityAtUtcSeconds` INTEGER NOT NULL, PRIMARY KEY(`userId`, `deviceId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localizedClientName", + "columnName": "localizedClientName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAtUtcSeconds", + "columnName": "createdAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "activatedAtUtcSeconds", + "columnName": "activatedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rejectedAtUtcSeconds", + "columnName": "rejectedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "activationToken", + "columnName": "activationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastActivityAtUtcSeconds", + "columnName": "lastActivityAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "deviceId" + ] + }, + "indices": [ + { + "name": "index_AuthDeviceEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AuthDeviceEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "MemberDeviceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `memberId` TEXT NOT NULL, `addressId` TEXT, `state` INTEGER NOT NULL, `name` TEXT NOT NULL, `localizedClientName` TEXT NOT NULL, `platform` TEXT, `createdAtUtcSeconds` INTEGER NOT NULL, `activatedAtUtcSeconds` INTEGER, `rejectedAtUtcSeconds` INTEGER, `activationToken` TEXT, `lastActivityAtUtcSeconds` INTEGER NOT NULL, PRIMARY KEY(`userId`, `deviceId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memberId", + "columnName": "memberId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localizedClientName", + "columnName": "localizedClientName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAtUtcSeconds", + "columnName": "createdAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "activatedAtUtcSeconds", + "columnName": "activatedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rejectedAtUtcSeconds", + "columnName": "rejectedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "activationToken", + "columnName": "activationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastActivityAtUtcSeconds", + "columnName": "lastActivityAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "deviceId" + ] + }, + "indices": [ + { + "name": "index_MemberDeviceEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_MemberDeviceEntity_memberId", + "unique": false, + "columnNames": [ + "memberId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_memberId` ON `${TABLE_NAME}` (`memberId`)" + }, + { + "name": "index_MemberDeviceEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + } + ], + "views": [], + "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, 'f040ffee30acc3a49d2e630101c22559')" + ] + } +} \ No newline at end of file diff --git a/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabase.kt b/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabase.kt index 44a18beb8..0722194fc 100644 --- a/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabase.kt +++ b/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabase.kt @@ -202,7 +202,7 @@ abstract class AccountManagerDatabase : companion object { const val name = "db-account-manager" - const val version = 56 + const val version = 57 val migrations = listOf( AccountManagerDatabaseMigrations.MIGRATION_1_2, @@ -259,7 +259,8 @@ abstract class AccountManagerDatabase : AccountManagerDatabaseMigrations.MIGRATION_52_53, AccountManagerDatabaseMigrations.MIGRATION_53_54, AccountManagerDatabaseMigrations.MIGRATION_54_55, - AccountManagerDatabaseMigrations.MIGRATION_55_56 + AccountManagerDatabaseMigrations.MIGRATION_55_56, + AccountManagerDatabaseMigrations.MIGRATION_56_57, ) fun databaseBuilder(context: Context): Builder = diff --git a/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabaseMigrations.kt b/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabaseMigrations.kt index 539cf0671..db9984363 100644 --- a/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabaseMigrations.kt +++ b/account-manager/data-db/src/main/kotlin/me/proton/core/accountmanager/data/db/AccountManagerDatabaseMigrations.kt @@ -392,4 +392,11 @@ object AccountManagerDatabaseMigrations { MailSettingsDatabase.MIGRATION_1.migrate(db) } } + + val MIGRATION_56_57 = object : Migration(56, 57) { + override fun migrate(db: SupportSQLiteDatabase) { + UserDatabase.MIGRATION_6.migrate(db) + AccountDatabase.MIGRATION_9.migrate(db) + } + } } diff --git a/account-manager/presentation-compose/src/test/kotlin/me/proton/core/accountmanager/presentation/compose/viewmodel/AccountSettingsViewModelTest.kt b/account-manager/presentation-compose/src/test/kotlin/me/proton/core/accountmanager/presentation/compose/viewmodel/AccountSettingsViewModelTest.kt index 40bb2f597..1b6e0a576 100644 --- a/account-manager/presentation-compose/src/test/kotlin/me/proton/core/accountmanager/presentation/compose/viewmodel/AccountSettingsViewModelTest.kt +++ b/account-manager/presentation-compose/src/test/kotlin/me/proton/core/accountmanager/presentation/compose/viewmodel/AccountSettingsViewModelTest.kt @@ -81,6 +81,7 @@ class AccountSettingsViewModelTest : CoroutinesTest by CoroutinesTest() { delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) diff --git a/account-recovery/domain/src/test/kotlin/me/proton/core/accountrecovery/domain/usecase/ObserveUserRecoveryStateTest.kt b/account-recovery/domain/src/test/kotlin/me/proton/core/accountrecovery/domain/usecase/ObserveUserRecoveryStateTest.kt index 1f478824a..71130b2a6 100644 --- a/account-recovery/domain/src/test/kotlin/me/proton/core/accountrecovery/domain/usecase/ObserveUserRecoveryStateTest.kt +++ b/account-recovery/domain/src/test/kotlin/me/proton/core/accountrecovery/domain/usecase/ObserveUserRecoveryStateTest.kt @@ -61,7 +61,8 @@ class ObserveUserRecoveryStateTest { sessionId = SessionId("test-session-id"), reason = UserRecovery.Reason.Authentication ), - keys = emptyList() + keys = emptyList(), + flags = emptyMap(), ) private val testUserNullRecovery = testUser.copy(recovery = null) diff --git a/account/data/src/main/kotlin/me/proton/core/account/data/db/AccountDatabase.kt b/account/data/src/main/kotlin/me/proton/core/account/data/db/AccountDatabase.kt index 9d54b6aa9..74dccd1fe 100644 --- a/account/data/src/main/kotlin/me/proton/core/account/data/db/AccountDatabase.kt +++ b/account/data/src/main/kotlin/me/proton/core/account/data/db/AccountDatabase.kt @@ -175,5 +175,17 @@ interface AccountDatabase : Database { ) } } + + /** + * - Added [User.flags], insert migration to populate the value from backend (see AccountMigrator). + */ + val MIGRATION_9 = object : DatabaseMigration { + override fun migrate(database: SupportSQLiteDatabase) { + // If there are no migrations the value is NULL. + database.execSQL("UPDATE AccountMetadataEntity SET migrations = IFNULL(migrations || ';RefreshUser', 'RefreshUser')") + database.execSQL("UPDATE AccountEntity SET state = 'MigrationNeeded' WHERE state = 'Ready'") + } + } + } } diff --git a/auth/data/src/main/kotlin/me/proton/core/auth/data/api/response/SRPAuthenticationResponse.kt b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/response/SRPAuthenticationResponse.kt index 97115dee8..dc226083b 100644 --- a/auth/data/src/main/kotlin/me/proton/core/auth/data/api/response/SRPAuthenticationResponse.kt +++ b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/response/SRPAuthenticationResponse.kt @@ -28,7 +28,7 @@ data class SRPAuthenticationResponse( @SerialName("Code") val code: Int, @SerialName("ServerProof") - val serverProof: ServerProof, + val serverProof: ServerProof? = null, ) fun SRPAuthenticationResponse.isSuccess(): Boolean = code == ResponseCodes.OK diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/LogTag.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/LogTag.kt index 66e3393c3..84df78a7e 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/LogTag.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/LogTag.kt @@ -26,4 +26,5 @@ object LogTag { const val ACTIVATE_DEVICE = "core.auth.domain.perform.backuppass.activate" const val UNPRIVATIZE_USER = "core.auth.domain.perform.backuppass.unprivatize" const val SETUP_KEYS = "core.auth.domain.perform.backuppass.setup.keys" + const val CHANGE_BACKUP_PASSWORD = "core.auth.domain.perform.backuppass.change" } diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginLessAccountSetup.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginLessAccountSetup.kt index 5e693650e..fb5513755 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginLessAccountSetup.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginLessAccountSetup.kt @@ -78,6 +78,7 @@ class PostLoginLessAccountSetup @Inject constructor( delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), maxBaseSpace = 0, maxDriveSpace = 0, usedBaseSpace = 0, diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/ValidateServerProof.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/ValidateServerProof.kt index ad7c6eaf8..8d905e0ae 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/ValidateServerProof.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/ValidateServerProof.kt @@ -30,7 +30,7 @@ class ValidateServerProof @Inject constructor() { * Throws an [InvalidServerAuthenticationException] with the result of calling [lazyMessage] * if the [ServerProof] isn't the one expected. */ - operator fun invoke(serverProof: ServerProof, expectedProof: String, lazyMessage: () -> Any) { + operator fun invoke(serverProof: ServerProof?, expectedProof: String?, lazyMessage: () -> Any) { if (serverProof != expectedProof) { val message = "Server returned invalid srp proof, ${lazyMessage.invoke()}" val exception = InvalidServerAuthenticationException(message) diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/ChangeBackupPassword.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/ChangeBackupPassword.kt new file mode 100644 index 000000000..bc3864db7 --- /dev/null +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/ChangeBackupPassword.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.domain.usecase.sso + +import me.proton.core.account.domain.repository.AccountRepository +import me.proton.core.auth.domain.repository.AuthRepository +import me.proton.core.auth.domain.repository.DeviceSecretRepository +import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.crypto.common.keystore.EncryptedString +import me.proton.core.crypto.common.keystore.decrypt +import me.proton.core.crypto.common.keystore.use +import me.proton.core.domain.entity.UserId +import me.proton.core.user.domain.UserManager +import me.proton.core.user.domain.extension.nameNotNull +import me.proton.core.user.domain.repository.PassphraseRepository +import me.proton.core.user.domain.repository.UserRepository +import javax.inject.Inject + +class ChangeBackupPassword @Inject constructor( + context: CryptoContext, + private val accountRepository: AccountRepository, + private val authRepository: AuthRepository, + private val userManager: UserManager, + private val userRepository: UserRepository, + private val passphraseRepository: PassphraseRepository, + private val deviceSecretRepository: DeviceSecretRepository, + private val getEncryptedSecret: GetEncryptedSecret, +) { + private val keyStore = context.keyStoreCrypto + private val srp = context.srpCrypto + + suspend operator fun invoke( + userId: UserId, + newBackupPassword: EncryptedString, + ): Boolean { + val user = userRepository.getUser(userId) + val username = user.nameNotNull() + val account = accountRepository.getAccountOrNull(userId) + val sessionId = requireNotNull(account?.sessionId) + val modulus = authRepository.randomModulus(sessionId) + val currentPassphrase = requireNotNull(passphraseRepository.getPassphrase(userId)) + val deviceSecret = requireNotNull(deviceSecretRepository.getByUserId(userId)?.secret) + currentPassphrase.decrypt(keyStore).use { decryptedCurrentPassphrase -> + newBackupPassword.decrypt(keyStore).toByteArray().use { decryptedBackupPassword -> + val auth = srp.calculatePasswordVerifier( + username = username, + password = decryptedBackupPassword.array, + modulusId = modulus.modulusId, + modulus = modulus.modulus + ) + return userManager.changePassword( + userId = userId, + newPassword = newBackupPassword, + secondFactorProof = null, + proofs = null, + srpSession = null, + auth = auth, + encryptedSecret = getEncryptedSecret.invoke( + passphrase = decryptedCurrentPassphrase, + deviceSecret = deviceSecret + ) + ) + } + } + } +} diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/CheckOtherDevices.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/CheckOtherDevices.kt index 4d78b1a33..61e9d8a42 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/CheckOtherDevices.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/CheckOtherDevices.kt @@ -42,8 +42,8 @@ class CheckOtherDevices @Inject constructor( val devices = authDeviceRepository.getByUserId(userId) val activeDevices = devices.filter { it.state == AuthDeviceState.Active } return when { - hasTemporaryPassword -> Result.AdminHelpRequired localDevice.isPendingAdmin() -> Result.AdminHelpRequested + hasTemporaryPassword -> Result.AdminHelpRequired activeDevices.isNotEmpty() -> Result.OtherDevicesAvailable(devices) else -> Result.BackupPassword } diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/GetEncryptedSecret.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/GetEncryptedSecret.kt index 2c172bd97..086a23228 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/GetEncryptedSecret.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/sso/GetEncryptedSecret.kt @@ -48,7 +48,7 @@ class GetEncryptedSecret @Inject constructor( operator fun invoke( passphrase: PlainByteArray, deviceSecret: DeviceSecretString - ): Based64EncodedAeadEncryptedSecret = pgpCrypto.getBase64Encoded( + ): Based64EncodedAeadEncryptedSecret = pgpCrypto.getBase64EncodedNoWrap( pgpCrypto.getBase64Decoded(deviceSecret.decrypt(keyStoreCrypto)).use { key -> passphrase.encrypt( crypto = aeadCrypto, diff --git a/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/AccountAvailabilityTest.kt b/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/AccountAvailabilityTest.kt index c62322e36..5dc966534 100644 --- a/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/AccountAvailabilityTest.kt +++ b/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/AccountAvailabilityTest.kt @@ -70,7 +70,8 @@ internal class AccountAvailabilityTest { delinquent = null, recovery = null, keys = emptyList(), - type = Type.Proton + type = Type.Proton, + flags = emptyMap(), ) @BeforeTest diff --git a/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/PostLoginSsoAccountSetupTest.kt b/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/PostLoginSsoAccountSetupTest.kt index e4595f530..fb367ad79 100644 --- a/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/PostLoginSsoAccountSetupTest.kt +++ b/auth/domain/src/test/kotlin/me/proton/core/auth/domain/usecase/PostLoginSsoAccountSetupTest.kt @@ -66,7 +66,9 @@ class PostLoginSsoAccountSetupTest { fun setUp() { accountWorkflowHandler = mockk() - user = mockk() + user = mockk { + every { flags } returns emptyMap() + } sessionId = mockk() userCheck = mockk { coEvery { this@mockk.invoke(any()) } returns PostLoginAccountSetup.UserCheckResult.Success diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/DeviceSecretScreen.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/DeviceSecretScreen.kt index 49a968031..85899c11f 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/DeviceSecretScreen.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/DeviceSecretScreen.kt @@ -51,6 +51,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import me.proton.core.auth.presentation.compose.DeviceSecretViewState.ChangePassword import me.proton.core.auth.presentation.compose.DeviceSecretViewState.Close import me.proton.core.auth.presentation.compose.DeviceSecretViewState.DeviceRejected import me.proton.core.auth.presentation.compose.DeviceSecretViewState.Error @@ -58,6 +59,7 @@ import me.proton.core.auth.presentation.compose.DeviceSecretViewState.FirstLogin import me.proton.core.auth.presentation.compose.DeviceSecretViewState.InvalidSecret import me.proton.core.auth.presentation.compose.DeviceSecretViewState.Loading import me.proton.core.auth.presentation.compose.DeviceSecretViewState.Success +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeScreen import me.proton.core.auth.presentation.compose.sso.BackupPasswordInputScreen import me.proton.core.auth.presentation.compose.sso.BackupPasswordSetupScreen import me.proton.core.auth.presentation.compose.sso.RequestAccessDeniedScreen @@ -125,12 +127,14 @@ public fun DeviceSecretScreen( is Close -> onClose() is Success -> onSuccess(state.userId) is Loading -> DeviceSecretScaffold( + modifier = modifier, onCloseClicked = onCloseClicked, onRetryClicked = onReloadState, isLoading = true, email = state.email ) is Error -> DeviceSecretScaffold( + modifier = modifier, onCloseClicked = onCloseClicked, onRetryClicked = onReloadState, email = state.email, @@ -169,6 +173,7 @@ public fun DeviceSecretScreen( ) is InvalidSecret.NoDevice.RequireAdmin -> RequestAdminHelpScreen( + modifier = modifier, onBackClicked = onCloseClicked, onErrorMessage = onErrorMessage, onSuccess = onReloadState, @@ -180,8 +185,13 @@ public fun DeviceSecretScreen( onBackToSignInClicked = onCloseClicked ) - // TODO: Replace with BackupPasswordChangeScreen() - is DeviceSecretViewState.ChangePassword -> onClose() + is ChangePassword -> BackupPasswordChangeScreen( + modifier = modifier, + onCloseClicked = onCloseClicked, + onCloseMessage = onCloseMessage, + onErrorMessage = onErrorMessage, + onSuccess = onReloadState, + ) } } diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeOperation.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeOperation.kt new file mode 100644 index 000000000..ab0bce257 --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeOperation.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.presentation.compose.sso + +public sealed interface BackupPasswordChangeOperation + +public sealed interface BackupPasswordChangeAction : BackupPasswordChangeOperation { + public data class ChangePassword( + val backupPassword: String, + val repeatBackupPassword: String, + val unused: Long = System.currentTimeMillis() + ) : BackupPasswordChangeAction +} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreen.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreen.kt new file mode 100644 index 000000000..d2e4c56fd --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreen.kt @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.presentation.compose.sso + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import me.proton.core.auth.presentation.compose.R +import me.proton.core.compose.component.ProtonPasswordOutlinedTextFieldWithError +import me.proton.core.compose.component.ProtonSolidButton +import me.proton.core.compose.component.appbar.ProtonTopAppBar +import me.proton.core.compose.theme.LocalColors +import me.proton.core.compose.theme.ProtonDimens +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.ProtonTypography + +@Composable +public fun BackupPasswordChangeScreen( + onCloseClicked: () -> Unit, + onCloseMessage: (String?) -> Unit, + onErrorMessage: (String?) -> Unit, + onSuccess: () -> Unit, + modifier: Modifier = Modifier, + viewModel: BackupPasswordChangeViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + BackupPasswordChangeScreen( + modifier = modifier, + onCloseClicked = onCloseClicked, + onContinueClicked = { viewModel.submit(it) }, + onCloseMessage = onCloseMessage, + onErrorMessage = onErrorMessage, + onSuccess = onSuccess, + state = state + ) +} + +@Composable +public fun BackupPasswordChangeScreen( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = {}, + onContinueClicked: (BackupPasswordChangeAction.ChangePassword) -> Unit = {}, + onCloseMessage: (String?) -> Unit = {}, + onErrorMessage: (String?) -> Unit = {}, + onSuccess: () -> Unit = {}, + state: BackupPasswordChangeState, +) { + LaunchedEffect(state) { + when (state) { + is BackupPasswordChangeState.Close -> onCloseMessage(state.message) + is BackupPasswordChangeState.Error -> onErrorMessage(state.message) + is BackupPasswordChangeState.Success -> onSuccess() + else -> Unit + } + } + BackupPasswordChangeScaffold( + modifier = modifier, + onCloseClicked = onCloseClicked, + onContinueClicked = onContinueClicked, + isPasswordTooShort = state.isPasswordTooShort(), + arePasswordsNotMatching = state.arePasswordsNotMatching(), + isLoading = state is BackupPasswordChangeState.Loading + ) +} + +@Composable +public fun BackupPasswordChangeScaffold( + modifier: Modifier = Modifier, + onCloseClicked: () -> Unit = {}, + onContinueClicked: (BackupPasswordChangeAction.ChangePassword) -> Unit = {}, + isPasswordTooShort: Boolean = false, + arePasswordsNotMatching: Boolean = false, + isLoading: Boolean = false, +) { + Scaffold( + modifier = modifier, + topBar = { + ProtonTopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onCloseClicked) { + Icon( + painterResource(id = R.drawable.ic_proton_close), + contentDescription = stringResource(id = R.string.presentation_close) + ) + } + }, + backgroundColor = LocalColors.current.backgroundNorm + ) + } + ) { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues) + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(ProtonDimens.DefaultSpacing), + ) { + + val errorTooShort = stringResource(R.string.backup_password_setup_password_too_short) + val errorNotMatch = stringResource(R.string.backup_password_setup_password_not_matching) + + BackupPasswordChangeForm( + backupPasswordError = errorTooShort.takeIf { isPasswordTooShort }, + backupPasswordRepeatedError = errorNotMatch.takeIf { arePasswordsNotMatching }, + onContinueClicked = onContinueClicked, + isLoading = isLoading, + ) + } + } + } +} + +@Composable +private fun BackupPasswordChangeForm( + backupPasswordError: String?, + backupPasswordRepeatedError: String?, + isLoading: Boolean, + onContinueClicked: (BackupPasswordChangeAction.ChangePassword) -> Unit, + modifier: Modifier = Modifier, +) { + var backupPassword by rememberSaveable { mutableStateOf("") } + var repeatBackupPassword by rememberSaveable { mutableStateOf("") } + + Column( + modifier = modifier + ) { + Text( + text = stringResource(id = R.string.backup_password_change_title), + style = ProtonTypography.Default.headline + ) + Text( + modifier = Modifier.padding(top = ProtonDimens.MediumSpacing), + text = stringResource(id = R.string.backup_password_change_description), + style = ProtonTypography.Default.body2Regular + ) + ProtonPasswordOutlinedTextFieldWithError( + text = backupPassword, + onValueChanged = { backupPassword = it }, + enabled = !isLoading, + singleLine = true, + label = { Text(text = stringResource(id = R.string.backup_password_setup_password_label)) }, + errorText = backupPasswordError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Next + ), + modifier = Modifier.padding(top = ProtonDimens.MediumSpacing) + ) + ProtonPasswordOutlinedTextFieldWithError( + text = repeatBackupPassword, + onValueChanged = { repeatBackupPassword = it }, + enabled = !isLoading, + singleLine = true, + label = { Text(text = stringResource(id = R.string.backup_password_setup_repeat_password_label)) }, + errorText = backupPasswordRepeatedError, + modifier = Modifier.padding(top = ProtonDimens.SmallSpacing) + ) + ProtonSolidButton( + contained = false, + loading = isLoading, + modifier = Modifier + .padding(top = ProtonDimens.MediumSpacing) + .height(ProtonDimens.DefaultButtonMinHeight), + onClick = { onContinueClicked(BackupPasswordChangeAction.ChangePassword(backupPassword, repeatBackupPassword)) } + ) { + Text(text = stringResource(id = R.string.backup_password_setup_continue_action)) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(device = Devices.TABLET) +@Composable +private fun BackupPasswordChangeScreenPreview() { + ProtonTheme { + BackupPasswordChangeScreen(state = BackupPasswordChangeState.Idle) + } +} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeState.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeState.kt new file mode 100644 index 000000000..52fe09fd3 --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeState.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.presentation.compose.sso + +public sealed interface BackupPasswordChangeState { + public data object Idle : BackupPasswordChangeState + public data object Loading : BackupPasswordChangeState + + public data class FormError(val cause: PasswordFormError) : BackupPasswordChangeState + + public data class Error(val message: String?) : BackupPasswordChangeState + public data class Close(val message: String?) : BackupPasswordChangeState + + public data object Success : BackupPasswordChangeState +} + +internal fun BackupPasswordChangeState.formErrorOrNull(): PasswordFormError? = + (this as? BackupPasswordChangeState.FormError)?.cause + +internal fun BackupPasswordChangeState.isPasswordTooShort(): Boolean = + formErrorOrNull() == PasswordFormError.PasswordTooShort + +internal fun BackupPasswordChangeState.arePasswordsNotMatching(): Boolean = + formErrorOrNull() == PasswordFormError.PasswordsDoNotMatch diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeViewModel.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeViewModel.kt new file mode 100644 index 000000000..2514b625b --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeViewModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.presentation.compose.sso + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import me.proton.core.auth.domain.LogTag +import me.proton.core.auth.domain.usecase.sso.ChangeBackupPassword +import me.proton.core.auth.presentation.compose.DeviceSecretRoutes.Arg.getUserId +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeAction.ChangePassword +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeState.Error +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeState.FormError +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeState.Idle +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeState.Loading +import me.proton.core.auth.presentation.compose.sso.BackupPasswordChangeState.Success +import me.proton.core.compose.viewmodel.stopTimeoutMillis +import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.crypto.common.keystore.encrypt +import me.proton.core.domain.entity.UserId +import me.proton.core.presentation.utils.InputValidationResult +import me.proton.core.presentation.utils.ValidationType +import me.proton.core.presentation.utils.onFailure +import me.proton.core.presentation.utils.onSuccess +import me.proton.core.util.kotlin.catchAll +import me.proton.core.util.kotlin.catchWhen +import javax.inject.Inject + +@HiltViewModel +public class BackupPasswordChangeViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val context: CryptoContext, + private val changeBackupPassword: ChangeBackupPassword +) : ViewModel() { + + private val userId: UserId by lazy { savedStateHandle.getUserId() } + + private val mutableAction = MutableStateFlow(null) + + public val state: StateFlow = mutableAction.flatMapLatest { action -> + when (action) { + null -> flowOf(Idle) + is ChangePassword -> onValidatePassword(action) + } + }.stateIn(viewModelScope, WhileSubscribed(stopTimeoutMillis), Idle) + + public fun submit(action: BackupPasswordChangeAction): Job = viewModelScope.launch { + mutableAction.emit(action) + } + + private fun onValidatePassword(action: ChangePassword) = flow { + emit(Loading) + InputValidationResult( + text = action.backupPassword, + validationType = ValidationType.PasswordMinLength + ).onFailure { + emit(FormError(PasswordFormError.PasswordTooShort)) + }.onSuccess { + InputValidationResult( + text = action.backupPassword, + validationType = ValidationType.PasswordMatch, + additionalText = action.repeatBackupPassword + ).onFailure { + emit(FormError(PasswordFormError.PasswordsDoNotMatch)) + }.onSuccess { + emitAll(onChangeBackupPassword(action.backupPassword)) + } + } + } + + private fun onChangeBackupPassword(backupPassword: String) = flow { + emit(Loading) + val password = backupPassword.encrypt(context.keyStoreCrypto) + changeBackupPassword.invoke(userId, password) + emit(Success) + }.catchWhen(Throwable::isActionNotAllowed) { + emit(BackupPasswordChangeState.Close(it.message)) + }.catchAll(LogTag.CHANGE_BACKUP_PASSWORD) { + emit(Error(it.message)) + } +} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordInputViewModel.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordInputViewModel.kt index 984d31728..bdabb03ae 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordInputViewModel.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordInputViewModel.kt @@ -112,9 +112,3 @@ public class BackupPasswordInputViewModel @Inject constructor( emit(BackupPasswordInputState.Error(it.message)) } } - -private fun Throwable.isActionNotAllowed(): Boolean { - if (this !is ApiException) return false - val error = error as? ApiResult.Error.Http - return error?.proton?.code == ResponseCodes.NOT_ALLOWED -} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreen.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreen.kt index 8136dce2e..a70a7b2e7 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreen.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreen.kt @@ -153,7 +153,7 @@ public fun BackupPasswordSetupScaffold( .verticalScroll(rememberScrollState()) .padding(ProtonDimens.DefaultSpacing), ) { - SsoOrganizationAdminInfoHeader( + OrganizationAdminInfoHeader( organizationAdminEmail = organizationAdminEmail, organizationIcon = organizationIcon, organizationName = organizationName @@ -166,7 +166,7 @@ public fun BackupPasswordSetupScaffold( val errorTooShort = stringResource(R.string.backup_password_setup_password_too_short) val errorNotMatch = stringResource(R.string.backup_password_setup_password_not_matching) - SsoBackupPasswordSetupForm( + BackupPasswordSetupForm( backupPasswordError = errorTooShort.takeIf { isPasswordTooShort }, backupPasswordRepeatedError = errorNotMatch.takeIf { arePasswordsNotMatching }, onContinueClicked = onContinueClicked, @@ -181,7 +181,7 @@ public fun BackupPasswordSetupScaffold( } @Composable -private fun SsoOrganizationAdminInfoHeader( +private fun OrganizationAdminInfoHeader( organizationAdminEmail: String?, organizationIcon: Any?, organizationName: String?, @@ -232,7 +232,7 @@ private fun SsoOrganizationAdminInfoHeader( } @Composable -private fun SsoBackupPasswordSetupForm( +private fun BackupPasswordSetupForm( backupPasswordError: String?, backupPasswordRepeatedError: String?, isLoading: Boolean, diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupState.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupState.kt index cddea1258..9cc695478 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupState.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupState.kt @@ -36,7 +36,7 @@ public sealed class BackupPasswordSetupState( public data class FormError( override val data: BackupPasswordSetupData, - val cause: BackupPasswordSetupFormError + val cause: PasswordFormError ) : BackupPasswordSetupState(data) public data class Success( @@ -44,16 +44,11 @@ public sealed class BackupPasswordSetupState( ) : BackupPasswordSetupState(data) } -internal fun BackupPasswordSetupState.formErrorOrNull(): BackupPasswordSetupFormError? = +internal fun BackupPasswordSetupState.formErrorOrNull(): PasswordFormError? = (this as? BackupPasswordSetupState.FormError)?.cause internal fun BackupPasswordSetupState.isPasswordTooShort(): Boolean = - formErrorOrNull() == BackupPasswordSetupFormError.PasswordTooShort + formErrorOrNull() == PasswordFormError.PasswordTooShort internal fun BackupPasswordSetupState.arePasswordsNotMatching(): Boolean = - formErrorOrNull() == BackupPasswordSetupFormError.PasswordsDoNotMatch - -public sealed interface BackupPasswordSetupFormError { - public data object PasswordTooShort : BackupPasswordSetupFormError - public data object PasswordsDoNotMatch : BackupPasswordSetupFormError -} + formErrorOrNull() == PasswordFormError.PasswordsDoNotMatch diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModel.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModel.kt index 2b1be5e0d..ede1d2e63 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModel.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModel.kt @@ -124,14 +124,14 @@ public class BackupPasswordSetupViewModel @Inject constructor( text = action.backupPassword, validationType = ValidationType.PasswordMinLength ).onFailure { - emit(FormError(state.value.data, BackupPasswordSetupFormError.PasswordTooShort)) + emit(FormError(state.value.data, PasswordFormError.PasswordTooShort)) }.onSuccess { InputValidationResult( text = action.backupPassword, validationType = ValidationType.PasswordMatch, additionalText = action.repeatBackupPassword ).onFailure { - emit(FormError(state.value.data, BackupPasswordSetupFormError.PasswordsDoNotMatch)) + emit(FormError(state.value.data, PasswordFormError.PasswordsDoNotMatch)) }.onSuccess { when (val organizationPublicKey = state.value.data.organizationPublicKey) { null -> emit(Error(state.value.data, null)) diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ConfirmationDigits.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ConfirmationDigits.kt index 64afb0844..ba936ea48 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ConfirmationDigits.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ConfirmationDigits.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -55,7 +57,6 @@ import me.proton.core.auth.presentation.compose.R import me.proton.core.auth.presentation.compose.SMALL_SCREEN_HEIGHT import me.proton.core.compose.theme.ProtonDimens import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.compose.theme.defaultNorm @Composable internal fun ConfirmationDigits( @@ -116,7 +117,8 @@ private fun ConfirmationCodeDigit( Text( textAlign = TextAlign.Center, text = digit.toString(), - style = textStyle + style = textStyle, + fontFamily = FontFamily.Monospace ) } } @@ -159,7 +161,7 @@ internal fun ConfirmationDigitTextField( true -> BorderStroke(1.dp, ProtonTheme.colors.interactionNorm) false -> BorderStroke(1.dp, ProtonTheme.colors.separatorNorm) }, - digitStyle = ProtonTheme.typography.defaultNorm + digitStyle = ProtonTheme.typography.hero.copy(fontWeight = FontWeight.Normal) ) } } diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/MemberApprovalScreen.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/MemberApprovalScreen.kt index 01a5bcf7c..768009490 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/MemberApprovalScreen.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/MemberApprovalScreen.kt @@ -191,7 +191,11 @@ private fun ConfirmationCodeInputScreen( ) { var confirmationCode by remember { mutableStateOf("") } var expanded by remember { mutableStateOf(false) } - var selected by remember(data) { mutableStateOf(data.pendingDevices.getOrNull(0)) } + var selected by remember { mutableStateOf(null) } + when (selected?.deviceId) { + null -> selected = data.pendingDevices.firstOrNull() + !in data.pendingDevices.map { it.deviceId } -> selected = data.pendingDevices.firstOrNull() + } Column(modifier = Modifier.padding(ProtonDimens.DefaultSpacing)) { Text( diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/PasswordFormError.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/PasswordFormError.kt new file mode 100644 index 000000000..bc4a52fa3 --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/PasswordFormError.kt @@ -0,0 +1,6 @@ +package me.proton.core.auth.presentation.compose.sso + +public sealed interface PasswordFormError { + public data object PasswordTooShort : PasswordFormError + public data object PasswordsDoNotMatch : PasswordFormError +} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/RequestAdminHelpViewModel.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/RequestAdminHelpViewModel.kt index 4a877efcb..9bab4933c 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/RequestAdminHelpViewModel.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/RequestAdminHelpViewModel.kt @@ -32,11 +32,13 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.proton.core.auth.domain.LogTag +import me.proton.core.auth.domain.entity.AuthDeviceState import me.proton.core.auth.domain.repository.AuthDeviceRepository import me.proton.core.auth.domain.repository.DeviceSecretRepository import me.proton.core.auth.presentation.compose.DeviceApprovalRoutes.Arg.getUserId import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpAction.Load import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpAction.Submit +import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpState.AdminHelpHelpRequested import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpState.Error import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpState.Idle import me.proton.core.auth.presentation.compose.sso.RequestAdminHelpState.Loading @@ -86,8 +88,12 @@ public class RequestAdminHelpViewModel @Inject constructor( private fun onSubmit() = flow { emit(Loading(state.value.data)) val deviceId = requireNotNull(deviceSecretRepository.getByUserId(userId)?.deviceId) + val device = authDeviceRepository.getByDeviceId(userId, deviceId) + check(device?.state != AuthDeviceState.PendingAdminActivation) { + "Device is already pending admin activation." + } authDeviceRepository.requestAdminHelp(userId, deviceId) - emit(RequestAdminHelpState.AdminHelpHelpRequested(state.value.data)) + emit(AdminHelpHelpRequested(state.value.data)) }.catch { error -> emit(Error(state.value.data, error)) } diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ThrowableExt.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ThrowableExt.kt new file mode 100644 index 000000000..d92bab509 --- /dev/null +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/ThrowableExt.kt @@ -0,0 +1,11 @@ +package me.proton.core.auth.presentation.compose.sso + +import me.proton.core.network.domain.ApiException +import me.proton.core.network.domain.ApiResult +import me.proton.core.network.domain.ResponseCodes + +public fun Throwable.isActionNotAllowed(): Boolean { + if (this !is ApiException) return false + val error = error as? ApiResult.Error.Http + return error?.proton?.code == ResponseCodes.NOT_ALLOWED +} diff --git a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/WaitingAdminScreen.kt b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/WaitingAdminScreen.kt index aef588d50..41928c599 100644 --- a/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/WaitingAdminScreen.kt +++ b/auth/presentation-compose/src/main/kotlin/me/proton/core/auth/presentation/compose/sso/WaitingAdminScreen.kt @@ -130,8 +130,7 @@ public fun WaitingAdminScaffold( if (username != null) { Text( - modifier = Modifier - .padding(top = ProtonDimens.MediumSpacing), + modifier = Modifier.padding(top = ProtonDimens.MediumSpacing), text = stringResource( id = R.string.auth_login_share_confirmation_code_with_admin_subtitle, username diff --git a/auth/presentation-compose/src/main/res/values/strings.xml b/auth/presentation-compose/src/main/res/values/strings.xml index 7ea70e03b..f238bec9d 100644 --- a/auth/presentation-compose/src/main/res/values/strings.xml +++ b/auth/presentation-compose/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ Backup password To make sure it’s really you trying to sign-in, please enter your backup password. Enter your backup password + Join %1$s %1$s added you to their Proton organization. Set a backup password to add an extra layer of protection. @@ -64,6 +65,10 @@ Continue Password should be at least 8 characters long. Passwords do not match. + + Set backup password + This password adds an extra layer of protection and allows you to sign in if you get locked out. Make sure to keep it somewhere safe. + Invalid backup password. No Primary Key. No Key Salts. diff --git a/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreenTest.kt b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreenTest.kt new file mode 100644 index 000000000..c5d720ca1 --- /dev/null +++ b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordChangeScreenTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ + +package me.proton.core.auth.presentation.compose.sso + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import me.proton.core.compose.theme.ProtonTheme +import org.junit.Rule +import kotlin.test.Test + +class BackupPasswordChangeScreenTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5, + theme = "ProtonTheme" + ) + + @Test + fun `idle screen`() { + paparazzi.snapshot { + ProtonTheme { + BackupPasswordChangeScreen( + state = BackupPasswordChangeState.Idle + ) + } + } + } + + @Test + fun `password too short`() { + paparazzi.snapshot { + ProtonTheme { + BackupPasswordChangeScreen( + state = BackupPasswordChangeState.FormError(PasswordFormError.PasswordTooShort) + ) + } + } + } + + @Test + fun `passwords do not match`() { + paparazzi.snapshot { + ProtonTheme { + BackupPasswordChangeScreen( + state = BackupPasswordChangeState.FormError(PasswordFormError.PasswordsDoNotMatch) + ) + } + } + } +} diff --git a/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreenTest.kt b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreenTest.kt index 6523b5ca3..4a84a329c 100644 --- a/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreenTest.kt +++ b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupScreenTest.kt @@ -73,7 +73,7 @@ class BackupPasswordSetupScreenTest { organizationIcon = null, organizationName = "Example Organization", ), - cause = BackupPasswordSetupFormError.PasswordTooShort + cause = PasswordFormError.PasswordTooShort ) ) } @@ -91,7 +91,7 @@ class BackupPasswordSetupScreenTest { organizationIcon = null, organizationName = "Example Organization", ), - cause = BackupPasswordSetupFormError.PasswordsDoNotMatch + cause = PasswordFormError.PasswordsDoNotMatch ) ) } diff --git a/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModelTest.kt b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModelTest.kt index aaca29146..5f66497d8 100644 --- a/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModelTest.kt +++ b/auth/presentation-compose/src/test/kotlin/me/proton/core/auth/presentation/compose/sso/BackupPasswordSetupViewModelTest.kt @@ -78,7 +78,7 @@ class BackupPasswordSetupViewModelTest : CoroutinesTest by CoroutinesTest() { assertEquals( expected = BackupPasswordSetupState.FormError( data = BackupPasswordSetupData(), - cause = BackupPasswordSetupFormError.PasswordTooShort + cause = PasswordFormError.PasswordTooShort ), actual = awaitItem() ) @@ -98,7 +98,7 @@ class BackupPasswordSetupViewModelTest : CoroutinesTest by CoroutinesTest() { assertEquals( expected = BackupPasswordSetupState.FormError( data = BackupPasswordSetupData(), - cause = BackupPasswordSetupFormError.PasswordsDoNotMatch + cause = PasswordFormError.PasswordsDoNotMatch ), actual = awaitItem() ) diff --git a/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_idle screen.png b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_idle screen.png new file mode 100644 index 000000000..0ca327fa6 --- /dev/null +++ b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_idle screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6466c90dc230eb54de9315d3b1ab4cd6f2fbede844a77ff7a211d8235e09075 +size 34830 diff --git a/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_password too short.png b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_password too short.png new file mode 100644 index 000000000..898eae5da --- /dev/null +++ b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_password too short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7921b5eb814192416f9b94ab0c7d23f612f52be1d1c8c6c97a8151d2542f304b +size 39800 diff --git a/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_passwords do not match.png b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_passwords do not match.png new file mode 100644 index 000000000..d03f53b5f --- /dev/null +++ b/auth/presentation-compose/src/test/snapshots/images/me.proton.core.auth.presentation.compose.sso_BackupPasswordChangeScreenTest_passwords do not match.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e400d8764979a996f762ff8c2829e8a47f50ba0fb5aea5f5f1ab4f2174ae50ed +size 38018 diff --git a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/ChooseExternalEmailViewModelTest.kt b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/ChooseExternalEmailViewModelTest.kt index 7281fd01a..ee3e1b248 100644 --- a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/ChooseExternalEmailViewModelTest.kt +++ b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/ChooseExternalEmailViewModelTest.kt @@ -95,6 +95,7 @@ class ChooseExternalEmailViewModelTest : ArchTest by ArchTest(), delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) diff --git a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt index 3853fecf1..16b3a4b11 100644 --- a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt +++ b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt @@ -167,6 +167,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) diff --git a/contact/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt b/contact/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt index 33caf58b1..e7e08fddd 100644 --- a/contact/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt +++ b/contact/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt @@ -84,6 +84,7 @@ fun UserId.userEntity() = UserEntity( delinquent = null, recovery = null, passphrase = null, + flags = emptyMap(), maxBaseSpace = null, maxDriveSpace = null, usedBaseSpace = null, diff --git a/coreexample/schemas/me.proton.android.core.coreexample.db.AppDatabase/57.json b/coreexample/schemas/me.proton.android.core.coreexample.db.AppDatabase/57.json new file mode 100644 index 000000000..0a9590759 --- /dev/null +++ b/coreexample/schemas/me.proton.android.core.coreexample.db.AppDatabase/57.json @@ -0,0 +1,3508 @@ +{ + "formatVersion": 1, + "database": { + "version": 57, + "identityHash": "f040ffee30acc3a49d2e630101c22559", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "product" + ] + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sessionId" + ] + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, `fido2AuthenticationOptionsJson` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fido2AuthenticationOptionsJson", + "columnName": "fido2AuthenticationOptionsJson", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sessionId" + ] + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `createdAtUtc` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `type` INTEGER, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, `flags` TEXT, `maxBaseSpace` INTEGER, `maxDriveSpace` INTEGER, `usedBaseSpace` INTEGER, `usedDriveSpace` INTEGER, `recovery_state` INTEGER, `recovery_startTime` INTEGER, `recovery_endTime` INTEGER, `recovery_sessionId` TEXT, `recovery_reason` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtUtc", + "columnName": "createdAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isPrivate", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxBaseSpace", + "columnName": "maxBaseSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxDriveSpace", + "columnName": "maxDriveSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedBaseSpace", + "columnName": "usedBaseSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDriveSpace", + "columnName": "usedDriveSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.state", + "columnName": "recovery_state", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.startTime", + "columnName": "recovery_startTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.endTime", + "columnName": "recovery_endTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recovery.sessionId", + "columnName": "recovery_sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recovery.reason", + "columnName": "recovery_reason", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, `recoverySecretHash` TEXT, `recoverySecretSignature` TEXT, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recoverySecretHash", + "columnName": "recoverySecretHash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recoverySecretSignature", + "columnName": "recoverySecretSignature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "keyId" + ] + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "addressId" + ] + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "keyId" + ] + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "keyId" + ] + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `ignoreKT` INTEGER, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignoreKT", + "columnName": "ignoreKT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email" + ] + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email", + "publicKey" + ] + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "PublicAddressInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `warnings` TEXT NOT NULL, `protonMx` INTEGER NOT NULL, `isProton` INTEGER NOT NULL, `addressSignedKeyList_data` TEXT, `addressSignedKeyList_signature` TEXT, `addressSignedKeyList_minEpochId` INTEGER, `addressSignedKeyList_maxEpochId` INTEGER, `addressSignedKeyList_expectedMinEpochId` INTEGER, `catchAllSignedKeyList_data` TEXT, `catchAllSignedKeyList_signature` TEXT, `catchAllSignedKeyList_minEpochId` INTEGER, `catchAllSignedKeyList_maxEpochId` INTEGER, `catchAllSignedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "warnings", + "columnName": "warnings", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protonMx", + "columnName": "protonMx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isProton", + "columnName": "isProton", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addressSignedKeyList.data", + "columnName": "addressSignedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.signature", + "columnName": "addressSignedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.minEpochId", + "columnName": "addressSignedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.maxEpochId", + "columnName": "addressSignedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addressSignedKeyList.expectedMinEpochId", + "columnName": "addressSignedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.data", + "columnName": "catchAllSignedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.signature", + "columnName": "catchAllSignedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.minEpochId", + "columnName": "catchAllSignedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.maxEpochId", + "columnName": "catchAllSignedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catchAllSignedKeyList.expectedMinEpochId", + "columnName": "catchAllSignedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email" + ] + }, + "indices": [ + { + "name": "index_PublicAddressInfoEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressInfoEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyDataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `emailAddressType` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `source` INTEGER, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressInfoEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emailAddressType", + "columnName": "emailAddressType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email", + "publicKey" + ] + }, + "indices": [ + { + "name": "index_PublicAddressKeyDataEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyDataEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationToken", + "columnName": "verificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "clientId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MailSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `autoSaveContacts` INTEGER, `composerMode` INTEGER, `messageButtons` INTEGER, `showImages` INTEGER, `showMoved` INTEGER, `viewMode` INTEGER, `viewLayout` INTEGER, `swipeLeft` INTEGER, `swipeRight` INTEGER, `shortcuts` INTEGER, `pmSignature` INTEGER, `numMessagePerPage` INTEGER, `autoDeleteSpamAndTrashDays` INTEGER, `draftMimeType` TEXT, `receiveMimeType` TEXT, `showMimeType` TEXT, `enableFolderColor` INTEGER, `inheritParentFolderColor` INTEGER, `rightToLeft` INTEGER, `attachPublicKey` INTEGER, `sign` INTEGER, `pgpScheme` INTEGER, `promptPin` INTEGER, `stickyLabels` INTEGER, `confirmLink` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "autoSaveContacts", + "columnName": "autoSaveContacts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "composerMode", + "columnName": "composerMode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageButtons", + "columnName": "messageButtons", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "showImages", + "columnName": "showImages", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "showMoved", + "columnName": "showMoved", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "viewMode", + "columnName": "viewMode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "viewLayout", + "columnName": "viewLayout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipeLeft", + "columnName": "swipeLeft", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipeRight", + "columnName": "swipeRight", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shortcuts", + "columnName": "shortcuts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pmSignature", + "columnName": "pmSignature", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numMessagePerPage", + "columnName": "numMessagePerPage", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "autoDeleteSpamAndTrashDays", + "columnName": "autoDeleteSpamAndTrashDays", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "draftMimeType", + "columnName": "draftMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "receiveMimeType", + "columnName": "receiveMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showMimeType", + "columnName": "showMimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableFolderColor", + "columnName": "enableFolderColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "inheritParentFolderColor", + "columnName": "inheritParentFolderColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rightToLeft", + "columnName": "rightToLeft", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachPublicKey", + "columnName": "attachPublicKey", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign", + "columnName": "sign", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pgpScheme", + "columnName": "pgpScheme", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "promptPin", + "columnName": "promptPin", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "stickyLabels", + "columnName": "stickyLabels", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "confirmLink", + "columnName": "confirmLink", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `density` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `earlyAccess` INTEGER, `deviceRecovery` INTEGER, `telemetry` INTEGER, `crashReports` INTEGER, `sessionAccountRecovery` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_registeredKeys` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deviceRecovery", + "columnName": "deviceRecovery", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "telemetry", + "columnName": "telemetry", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "crashReports", + "columnName": "crashReports", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sessionAccountRecovery", + "columnName": "sessionAccountRecovery", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.registeredKeys", + "columnName": "twoFA_registeredKeys", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxCalendars", + "columnName": "maxCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedCalendars", + "columnName": "usedCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ContactEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `contactId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`contactId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactId" + ] + }, + "indices": [ + { + "name": "index_ContactEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ContactCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contactId` TEXT NOT NULL, `type` INTEGER NOT NULL, `data` TEXT NOT NULL, `signature` TEXT, `cardId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`contactId`) REFERENCES `ContactEntity`(`contactId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cardId", + "columnName": "cardId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "cardId" + ] + }, + "indices": [ + { + "name": "index_ContactCardEntity_contactId", + "unique": false, + "columnNames": [ + "contactId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactCardEntity_contactId` ON `${TABLE_NAME}` (`contactId`)" + } + ], + "foreignKeys": [ + { + "table": "ContactEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactId" + ], + "referencedColumns": [ + "contactId" + ] + } + ] + }, + { + "tableName": "ContactEmailEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `contactEmailId` TEXT NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `defaults` INTEGER NOT NULL, `order` INTEGER NOT NULL, `contactId` TEXT NOT NULL, `canonicalEmail` TEXT, `isProton` INTEGER, `lastUsedTime` INTEGER NOT NULL, PRIMARY KEY(`contactEmailId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contactId`) REFERENCES `ContactEntity`(`contactId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactEmailId", + "columnName": "contactEmailId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaults", + "columnName": "defaults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "canonicalEmail", + "columnName": "canonicalEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isProton", + "columnName": "isProton", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastUsedTime", + "columnName": "lastUsedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactEmailId" + ] + }, + "indices": [ + { + "name": "index_ContactEmailEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEmailEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_ContactEmailEntity_contactId", + "unique": false, + "columnNames": [ + "contactId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ContactEmailEntity_contactId` ON `${TABLE_NAME}` (`contactId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ContactEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactId" + ], + "referencedColumns": [ + "contactId" + ] + } + ] + }, + { + "tableName": "ContactEmailLabelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contactEmailId` TEXT NOT NULL, `labelId` TEXT NOT NULL, PRIMARY KEY(`contactEmailId`, `labelId`), FOREIGN KEY(`contactEmailId`) REFERENCES `ContactEmailEntity`(`contactEmailId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "contactEmailId", + "columnName": "contactEmailId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "labelId", + "columnName": "labelId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contactEmailId", + "labelId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ContactEmailEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contactEmailId" + ], + "referencedColumns": [ + "contactEmailId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, `fetchedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fetchedAt", + "columnName": "fetchedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "config" + ] + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LabelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `labelId` TEXT NOT NULL, `parentId` TEXT, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `path` TEXT NOT NULL, `color` TEXT NOT NULL, `order` INTEGER NOT NULL, `isNotified` INTEGER, `isExpanded` INTEGER, `isSticky` INTEGER, PRIMARY KEY(`userId`, `labelId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "labelId", + "columnName": "labelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "isNotified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isExpanded", + "columnName": "isExpanded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSticky", + "columnName": "isSticky", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "labelId" + ] + }, + "indices": [ + { + "name": "index_LabelEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_LabelEntity_labelId", + "unique": false, + "columnNames": [ + "labelId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_labelId` ON `${TABLE_NAME}` (`labelId`)" + }, + { + "name": "index_LabelEntity_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_parentId` ON `${TABLE_NAME}` (`parentId`)" + }, + { + "name": "index_LabelEntity_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_LabelEntity_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LabelEntity_type` ON `${TABLE_NAME}` (`type`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "FeatureFlagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "featureId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "featureId" + ] + }, + "indices": [ + { + "name": "index_FeatureFlagEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_FeatureFlagEntity_featureId", + "unique": false, + "columnNames": [ + "featureId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ChallengeFrameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))", + "fields": [ + { + "fieldPath": "challengeFrame", + "columnName": "challengeFrame", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flow", + "columnName": "flow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "focusTime", + "columnName": "focusTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clicks", + "columnName": "clicks", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "copy", + "columnName": "copy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paste", + "columnName": "paste", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keys", + "columnName": "keys", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "challengeFrame" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PushEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `pushId` TEXT NOT NULL, `objectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `pushId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushId", + "columnName": "pushId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "pushId" + ] + }, + "indices": [ + { + "name": "index_PushEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PushEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_PushEntity_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PushEntity_type` ON `${TABLE_NAME}` (`type`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "PurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `planName` TEXT NOT NULL, `planCycle` INTEGER NOT NULL, `purchaseState` TEXT NOT NULL, `purchaseFailure` TEXT, `paymentProvider` TEXT NOT NULL, `paymentOrderId` TEXT, `paymentToken` TEXT, `paymentCurrency` TEXT NOT NULL, `paymentAmount` INTEGER NOT NULL, PRIMARY KEY(`planName`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "planCycle", + "columnName": "planCycle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseState", + "columnName": "purchaseState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "purchaseFailure", + "columnName": "purchaseFailure", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentProvider", + "columnName": "paymentProvider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentOrderId", + "columnName": "paymentOrderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "paymentCurrency", + "columnName": "paymentCurrency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentAmount", + "columnName": "paymentAmount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "planName" + ] + }, + "indices": [ + { + "name": "index_PurchaseEntity_planName", + "unique": false, + "columnNames": [ + "planName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_planName` ON `${TABLE_NAME}` (`planName`)" + }, + { + "name": "index_PurchaseEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_PurchaseEntity_purchaseState", + "unique": false, + "columnNames": [ + "purchaseState" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_purchaseState` ON `${TABLE_NAME}` (`purchaseState`)" + }, + { + "name": "index_PurchaseEntity_paymentProvider", + "unique": false, + "columnNames": [ + "paymentProvider" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PurchaseEntity_paymentProvider` ON `${TABLE_NAME}` (`paymentProvider`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "GooglePurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))", + "fields": [ + { + "fieldPath": "googlePurchaseToken", + "columnName": "googlePurchaseToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "googlePurchaseToken" + ] + }, + "indices": [ + { + "name": "index_GooglePurchaseEntity_paymentToken", + "unique": true, + "columnNames": [ + "paymentToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ObservabilityEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TelemetryEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `group` TEXT NOT NULL, `name` TEXT NOT NULL, `values` TEXT NOT NULL, `dimensions` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dimensions", + "columnName": "dimensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TelemetryEventEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TelemetryEventEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `changeId` TEXT NOT NULL, `counterEncrypted` TEXT NOT NULL, `emailEncrypted` TEXT NOT NULL, `epochIdEncrypted` TEXT NOT NULL, `creationTimestampEncrypted` TEXT NOT NULL, `publicKeysEncrypted` TEXT NOT NULL, `isObsolete` TEXT NOT NULL, PRIMARY KEY(`userId`, `changeId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "changeId", + "columnName": "changeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "counterEncrypted", + "columnName": "counterEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emailEncrypted", + "columnName": "emailEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "epochIdEncrypted", + "columnName": "epochIdEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTimestampEncrypted", + "columnName": "creationTimestampEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKeysEncrypted", + "columnName": "publicKeysEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isObsolete", + "columnName": "isObsolete", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "changeId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SelfAuditResultEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "NotificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, `userId` TEXT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL, `payload` TEXT NOT NULL, PRIMARY KEY(`userId`, `notificationId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payload", + "columnName": "payload", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_NotificationEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_NotificationEntity_notificationId", + "unique": false, + "columnNames": [ + "notificationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_notificationId` ON `${TABLE_NAME}` (`notificationId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "RecoveryFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `createdAtUtcMillis` INTEGER NOT NULL, `keyCount` INTEGER, `recoveryFile` TEXT NOT NULL, `recoverySecretHash` TEXT NOT NULL, PRIMARY KEY(`recoverySecretHash`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAtUtcMillis", + "columnName": "createdAtUtcMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyCount", + "columnName": "keyCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recoveryFile", + "columnName": "recoveryFile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recoverySecretHash", + "columnName": "recoverySecretHash", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recoverySecretHash" + ] + }, + "indices": [ + { + "name": "index_RecoveryFileEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_RecoveryFileEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DeviceSecretEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `secret` TEXT NOT NULL, `token` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_DeviceSecretEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DeviceSecretEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AuthDeviceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `addressId` TEXT, `state` INTEGER NOT NULL, `name` TEXT NOT NULL, `localizedClientName` TEXT NOT NULL, `platform` TEXT, `createdAtUtcSeconds` INTEGER NOT NULL, `activatedAtUtcSeconds` INTEGER, `rejectedAtUtcSeconds` INTEGER, `activationToken` TEXT, `lastActivityAtUtcSeconds` INTEGER NOT NULL, PRIMARY KEY(`userId`, `deviceId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localizedClientName", + "columnName": "localizedClientName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAtUtcSeconds", + "columnName": "createdAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "activatedAtUtcSeconds", + "columnName": "activatedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rejectedAtUtcSeconds", + "columnName": "rejectedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "activationToken", + "columnName": "activationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastActivityAtUtcSeconds", + "columnName": "lastActivityAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "deviceId" + ] + }, + "indices": [ + { + "name": "index_AuthDeviceEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AuthDeviceEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "MemberDeviceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `memberId` TEXT NOT NULL, `addressId` TEXT, `state` INTEGER NOT NULL, `name` TEXT NOT NULL, `localizedClientName` TEXT NOT NULL, `platform` TEXT, `createdAtUtcSeconds` INTEGER NOT NULL, `activatedAtUtcSeconds` INTEGER, `rejectedAtUtcSeconds` INTEGER, `activationToken` TEXT, `lastActivityAtUtcSeconds` INTEGER NOT NULL, PRIMARY KEY(`userId`, `deviceId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memberId", + "columnName": "memberId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localizedClientName", + "columnName": "localizedClientName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAtUtcSeconds", + "columnName": "createdAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "activatedAtUtcSeconds", + "columnName": "activatedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rejectedAtUtcSeconds", + "columnName": "rejectedAtUtcSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "activationToken", + "columnName": "activationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastActivityAtUtcSeconds", + "columnName": "lastActivityAtUtcSeconds", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "deviceId" + ] + }, + "indices": [ + { + "name": "index_MemberDeviceEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_MemberDeviceEntity_memberId", + "unique": false, + "columnNames": [ + "memberId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_memberId` ON `${TABLE_NAME}` (`memberId`)" + }, + { + "name": "index_MemberDeviceEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemberDeviceEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + } + ], + "views": [], + "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, 'f040ffee30acc3a49d2e630101c22559')" + ] + } +} \ No newline at end of file diff --git a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabase.kt b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabase.kt index 7f938cca4..a034ed529 100644 --- a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabase.kt +++ b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabase.kt @@ -202,7 +202,7 @@ abstract class AppDatabase : companion object { const val name = "db-account-manager" - const val version = 56 + const val version = 57 val migrations = listOf( AppDatabaseMigrations.MIGRATION_1_2, @@ -259,7 +259,8 @@ abstract class AppDatabase : AppDatabaseMigrations.MIGRATION_52_53, AppDatabaseMigrations.MIGRATION_53_54, AppDatabaseMigrations.MIGRATION_54_55, - AppDatabaseMigrations.MIGRATION_55_56 + AppDatabaseMigrations.MIGRATION_55_56, + AppDatabaseMigrations.MIGRATION_56_57, ) fun buildDatabase(context: Context): AppDatabase = diff --git a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabaseMigrations.kt b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabaseMigrations.kt index f6a4633ff..c778bcd39 100644 --- a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabaseMigrations.kt +++ b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/db/AppDatabaseMigrations.kt @@ -393,4 +393,11 @@ object AppDatabaseMigrations { MailSettingsDatabase.MIGRATION_1.migrate(db) } } + + val MIGRATION_56_57 = object : Migration(56, 57) { + override fun migrate(db: SupportSQLiteDatabase) { + UserDatabase.MIGRATION_6.migrate(db) + AccountDatabase.MIGRATION_9.migrate(db) + } + } } diff --git a/data-room/src/main/kotlin/me/proton/core/data/room/db/CommonConverters.kt b/data-room/src/main/kotlin/me/proton/core/data/room/db/CommonConverters.kt index 9b05d4636..83a9e7b6d 100644 --- a/data-room/src/main/kotlin/me/proton/core/data/room/db/CommonConverters.kt +++ b/data-room/src/main/kotlin/me/proton/core/data/room/db/CommonConverters.kt @@ -22,6 +22,8 @@ import androidx.room.TypeConverter import me.proton.core.domain.entity.Product import me.proton.core.domain.entity.UserId import me.proton.core.network.domain.session.SessionId +import me.proton.core.util.kotlin.deserializeMapOrNull +import me.proton.core.util.kotlin.serialize class CommonConverters { @@ -37,6 +39,12 @@ class CommonConverters { @TypeConverter fun fromStringToListOfInt(value: String?): List? = Companion.fromStringToListOfInt(value) + @TypeConverter + fun fromMapOfStringBooleanToString(value: Map?): String? = value.orEmpty().serialize() + + @TypeConverter + fun fromStringToMapOfStringBoolean(value: String?): Map? = value?.deserializeMapOrNull().orEmpty() + @TypeConverter fun fromProductToString(value: Product?): String? = value?.name diff --git a/key/data/src/main/kotlin/me/proton/core/key/data/api/request/UpdateKeysForPasswordChangeRequest.kt b/key/data/src/main/kotlin/me/proton/core/key/data/api/request/UpdateKeysForPasswordChangeRequest.kt index fcf6aff86..95d881e50 100644 --- a/key/data/src/main/kotlin/me/proton/core/key/data/api/request/UpdateKeysForPasswordChangeRequest.kt +++ b/key/data/src/main/kotlin/me/proton/core/key/data/api/request/UpdateKeysForPasswordChangeRequest.kt @@ -21,19 +21,20 @@ package me.proton.core.key.data.api.request import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import me.proton.core.auth.data.api.request.Fido2Request +import me.proton.core.crypto.common.pgp.Based64Encoded @Serializable data class UpdateKeysForPasswordChangeRequest( @SerialName("KeySalt") val keySalt: String, @SerialName("ClientEphemeral") - val clientEphemeral: String, + val clientEphemeral: String? = null, @SerialName("ClientProof") - val clientProof: String, + val clientProof: String? = null, @SerialName("SRPSession") - val srpSession: String, + val srpSession: String? = null, @SerialName("TwoFactorCode") - val twoFactorCode: String?, + val twoFactorCode: String? = null, @SerialName("FIDO2") val fido2: Fido2Request? = null, @SerialName("Auth") @@ -41,7 +42,9 @@ data class UpdateKeysForPasswordChangeRequest( @SerialName("Keys") val keys: List? = null, @SerialName("UserKeys") - val userKeys: List? = null + val userKeys: List? = null, + @SerialName("EncryptedSecret") + val encryptedSecret: Based64Encoded? = null, ) @Serializable @@ -51,4 +54,3 @@ data class PrivateKeyRequest( @SerialName("ID") val id: String ) - diff --git a/key/data/src/main/kotlin/me/proton/core/key/data/api/response/UserResponse.kt b/key/data/src/main/kotlin/me/proton/core/key/data/api/response/UserResponse.kt index f63dd3dbd..9ac63c1bb 100644 --- a/key/data/src/main/kotlin/me/proton/core/key/data/api/response/UserResponse.kt +++ b/key/data/src/main/kotlin/me/proton/core/key/data/api/response/UserResponse.kt @@ -65,6 +65,8 @@ data class UserResponse( val recovery: UserRecoveryResponse? = null, @SerialName("Keys") val keys: List, + @SerialName("Flags") + val flags: Map? = null, @SerialName("MaxBaseSpace") val maxBaseSpace: Long? = null, @SerialName("MaxDriveSpace") diff --git a/key/data/src/main/kotlin/me/proton/core/key/data/repository/PrivateKeyRepositoryImpl.kt b/key/data/src/main/kotlin/me/proton/core/key/data/repository/PrivateKeyRepositoryImpl.kt index ccf6411e1..bc35e8428 100644 --- a/key/data/src/main/kotlin/me/proton/core/key/data/repository/PrivateKeyRepositoryImpl.kt +++ b/key/data/src/main/kotlin/me/proton/core/key/data/repository/PrivateKeyRepositoryImpl.kt @@ -109,19 +109,20 @@ class PrivateKeyRepositoryImpl @Inject constructor( override suspend fun updatePrivateKeys( sessionUserId: SessionUserId, keySalt: String, - srpProofs: SrpProofs, - srpSession: String, + srpProofs: SrpProofs?, + srpSession: String?, secondFactorProof: SecondFactorProof?, auth: Auth?, keys: List?, - userKeys: List? + userKeys: List?, + encryptedSecret: Based64Encoded? ): Boolean { return provider.get(sessionUserId).invoke { val response = updatePrivateKeys( UpdateKeysForPasswordChangeRequest( keySalt = keySalt, - clientEphemeral = srpProofs.clientEphemeral, - clientProof = srpProofs.clientProof, + clientEphemeral = srpProofs?.clientEphemeral, + clientProof = srpProofs?.clientProof, srpSession = srpSession, twoFactorCode = secondFactorProof.toSecondFactorCode(), fido2 = secondFactorProof.toSecondFactorFido(), @@ -131,10 +132,11 @@ class PrivateKeyRepositoryImpl @Inject constructor( }, userKeys = userKeys?.map { PrivateKeyRequest(privateKey = it.privateKey, id = it.keyId.id) - } + }, + encryptedSecret = encryptedSecret ) ) - validateServerProof(response.serverProof, srpProofs.expectedServerProof) { "key update failed" } + validateServerProof(response.serverProof, srpProofs?.expectedServerProof) { "key update failed" } response.isSuccess() }.valueOrThrow } diff --git a/key/domain/src/main/kotlin/me/proton/core/key/domain/repository/PrivateKeyRepository.kt b/key/domain/src/main/kotlin/me/proton/core/key/domain/repository/PrivateKeyRepository.kt index 7fc755da2..d5d89b3ae 100644 --- a/key/domain/src/main/kotlin/me/proton/core/key/domain/repository/PrivateKeyRepository.kt +++ b/key/domain/src/main/kotlin/me/proton/core/key/domain/repository/PrivateKeyRepository.kt @@ -59,12 +59,13 @@ interface PrivateKeyRepository { suspend fun updatePrivateKeys( sessionUserId: SessionUserId, keySalt: String, - srpProofs: SrpProofs, - srpSession: String, - secondFactorProof: SecondFactorProof?, - auth: Auth?, + srpProofs: SrpProofs? = null, + srpSession: String? = null, + secondFactorProof: SecondFactorProof? = null, + auth: Auth? = null, keys: List? = null, - userKeys: List? = null + userKeys: List? = null, + encryptedSecret: Based64Encoded? = null ): Boolean suspend fun reactivatePrivateKey( diff --git a/label/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt b/label/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt index f8fb29255..7cb585f55 100644 --- a/label/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt +++ b/label/data/src/test/kotlin/me/proton/core/contact/tests/SampleData.kt @@ -56,6 +56,7 @@ fun UserId.userEntity() = UserEntity( delinquent = null, recovery = null, passphrase = null, + flags = emptyMap(), maxBaseSpace = null, maxDriveSpace = null, usedBaseSpace = null, diff --git a/notification/data/src/test/kotlin/me/proton/core/notification/data/repository/testing/test_entities.kt b/notification/data/src/test/kotlin/me/proton/core/notification/data/repository/testing/test_entities.kt index 460fca8f4..a56dd9858 100644 --- a/notification/data/src/test/kotlin/me/proton/core/notification/data/repository/testing/test_entities.kt +++ b/notification/data/src/test/kotlin/me/proton/core/notification/data/repository/testing/test_entities.kt @@ -48,6 +48,7 @@ internal fun testUserEntity(userId: UserId) = UserEntity( delinquent = null, recovery = null, passphrase = null, + flags = emptyMap(), maxBaseSpace = null, maxDriveSpace = null, usedBaseSpace = null, diff --git a/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/viewmodel/UpgradePlansViewModelTest.kt b/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/viewmodel/UpgradePlansViewModelTest.kt index 90b00bff9..b16e729a3 100644 --- a/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/viewmodel/UpgradePlansViewModelTest.kt +++ b/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/viewmodel/UpgradePlansViewModelTest.kt @@ -213,6 +213,7 @@ class UpgradePlansViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coro delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) diff --git a/push/data/src/test/kotlin/me/proton/core/push/data/testing/test_entities.kt b/push/data/src/test/kotlin/me/proton/core/push/data/testing/test_entities.kt index 948d5437c..6aff07837 100644 --- a/push/data/src/test/kotlin/me/proton/core/push/data/testing/test_entities.kt +++ b/push/data/src/test/kotlin/me/proton/core/push/data/testing/test_entities.kt @@ -48,6 +48,7 @@ internal fun testUserEntity(userId: UserId) = UserEntity( delinquent = null, recovery = null, passphrase = null, + flags = emptyMap(), maxBaseSpace = null, maxDriveSpace = null, usedBaseSpace = null, diff --git a/user-settings/domain/src/main/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPassword.kt b/user-settings/domain/src/main/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPassword.kt index cf21893cc..f1bb1de6c 100644 --- a/user-settings/domain/src/main/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPassword.kt +++ b/user-settings/domain/src/main/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPassword.kt @@ -86,7 +86,8 @@ class PerformUpdateUserPassword @Inject constructor( secondFactorProof = secondFactorProof, proofs = clientProofs, srpSession = loginInfo.srpSession, - auth = auth + auth = auth, + encryptedSecret = null ) } } diff --git a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformResetUserPasswordTest.kt b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformResetUserPasswordTest.kt index c6dfdec45..a930ceeb4 100644 --- a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformResetUserPasswordTest.kt +++ b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformResetUserPasswordTest.kt @@ -91,7 +91,8 @@ class PerformResetUserPasswordTest { subscribed = 0, delinquent = null, recovery = null, - keys = emptyList() + keys = emptyList(), + flags = emptyMap(), ) // endregion diff --git a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateLoginPasswordTest.kt b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateLoginPasswordTest.kt index 5d64e581e..7db79e99f 100644 --- a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateLoginPasswordTest.kt +++ b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateLoginPasswordTest.kt @@ -95,7 +95,8 @@ class PerformUpdateLoginPasswordTest { subscribed = 0, delinquent = null, recovery = null, - keys = emptyList() + keys = emptyList(), + flags = emptyMap(), ) private val testUserSettingsResponse = UserSettings.nil(testUserId).copy( diff --git a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPasswordTest.kt b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPasswordTest.kt index fab057c64..63235851f 100644 --- a/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPasswordTest.kt +++ b/user-settings/domain/src/test/kotlin/me/proton/core/usersettings/domain/usecase/PerformUpdateUserPasswordTest.kt @@ -93,7 +93,8 @@ class PerformUpdateUserPasswordTest { subscribed = 0, delinquent = null, recovery = null, - keys = emptyList() + keys = emptyList(), + flags = emptyMap(), ) // endregion @@ -121,7 +122,8 @@ class PerformUpdateUserPasswordTest { secondFactorProof = any(), proofs = any(), srpSession = any(), - auth = any() + auth = any(), + encryptedSecret = any() ) } returns true @@ -199,7 +201,8 @@ class PerformUpdateUserPasswordTest { secondFactorProof = secondFactorFido, proofs = any(), srpSession = any(), - auth = any() + auth = any(), + encryptedSecret = any() ) } returns true diff --git a/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/ui/PasswordManagementFragment.kt b/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/ui/PasswordManagementFragment.kt index 85dd894de..22b81be63 100644 --- a/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/ui/PasswordManagementFragment.kt +++ b/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/ui/PasswordManagementFragment.kt @@ -184,6 +184,7 @@ class PasswordManagementFragment : showSuccess() } + is PasswordManagementViewModel.State.CannotChangePassword -> showError(getString(R.string.settings_password_management_cannot_change_password)) is PasswordManagementViewModel.State.Error -> showError(it.error.getUserMessage(resources)) } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModel.kt b/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModel.kt index 0b2240adc..6bded8c4f 100644 --- a/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModel.kt +++ b/user-settings/presentation/src/main/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModel.kt @@ -45,6 +45,8 @@ import me.proton.core.observability.domain.ObservabilityManager import me.proton.core.observability.domain.metrics.AccountRecoveryResetTotal import me.proton.core.presentation.viewmodel.ProtonViewModel import me.proton.core.user.domain.entity.UserRecovery +import me.proton.core.user.domain.extension.isSso +import me.proton.core.user.domain.usecase.ObserveUser import me.proton.core.usersettings.domain.usecase.IsSessionAccountRecoveryEnabled import me.proton.core.usersettings.domain.usecase.ObserveUserSettings import me.proton.core.usersettings.domain.usecase.PerformUpdateLoginPassword @@ -56,6 +58,7 @@ import javax.inject.Inject @HiltViewModel class PasswordManagementViewModel @Inject constructor( private val keyStoreCrypto: KeyStoreCrypto, + private val observeUser: ObserveUser, private val observeUserRecovery: ObserveUserRecovery, private val observeUserSettings: ObserveUserSettings, private val observeUserRecoverySelfInitiated: ObserveUserRecoverySelfInitiated, @@ -73,6 +76,10 @@ class PasswordManagementViewModel @Inject constructor( private val currentAction = MutableStateFlow(null) private val currentUserId = MutableStateFlow(null) + private val currentUser = currentUserId.filterNotNull().flatMapLatest { + observeUser(it) + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) + private val currentUserRecovery = currentUserId.filterNotNull().flatMapLatest { observeUserRecovery(it) }.stateIn(viewModelScope, SharingStarted.Eagerly, null) @@ -94,7 +101,7 @@ class PasswordManagementViewModel @Inject constructor( userId = userId, loginPasswordAvailable = true, mailboxPasswordAvailable = isMailboxPassword && product != Product.Vpn, - recoveryResetAvailable = recoveryResetAvailable(userId), + recoveryResetAvailable = recoveryResetAvailable(userId) && !isUserSso, recoveryResetEnabled = isRecoveryResetEnabled, currentLoginPasswordNeeded = !isRecoveryResetEnabled || !isRecoveryInsecure || !isSelfInitiated, twoFactorEnabled = isTwoFactorEnabled, @@ -117,6 +124,8 @@ class PasswordManagementViewModel @Inject constructor( resetPassword(action) } + isUserSso -> updateBackupPassword(action) + else -> when (action.type) { PasswordType.Login -> when (isMailboxPassword) { true -> updateLoginPassword(action) @@ -127,6 +136,12 @@ class PasswordManagementViewModel @Inject constructor( } } + private fun updateBackupPassword(action: Action.UpdatePassword): Flow = flow { + // Currently not supported to change password for Global SSO user. + emit(State.CannotChangePassword) + perform(Action.ObserveState(action.userId)) + } + private fun updateLoginPassword(action: Action.UpdatePassword): Flow = flow { emit(State.UpdatingPassword) val encryptedPassword = action.password.encrypt(keyStoreCrypto) @@ -190,8 +205,10 @@ class PasswordManagementViewModel @Inject constructor( return observeState(action.userId) } + private val user get() = currentUser.value private val userRecovery get() = currentUserRecovery.value private val userSettings get() = currentUserSettings.value + private val isUserSso get() = user?.isSso() ?: false private val isSelfInitiated get() = currentSelfInitiated.value private val isMailboxPassword get() = userSettings?.password?.mode == 2 private val isTwoFactorEnabled get() = userSettings?.twoFA?.enabled ?: false @@ -241,6 +258,7 @@ class PasswordManagementViewModel @Inject constructor( data object TwoFactorNeeded : State() data object UpdatingPassword : State() + data object CannotChangePassword : State() data object Success : State() data class Error(val error: Throwable) : State() } diff --git a/user-settings/presentation/src/main/res/values/strings.xml b/user-settings/presentation/src/main/res/values/strings.xml index 97b3082b3..36015b464 100644 --- a/user-settings/presentation/src/main/res/values/strings.xml +++ b/user-settings/presentation/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Second password To change either of your passwords, you\'ll need to know your main password. Forgot your main password? Change password + You can\'t change your backup password. Please contact your administrator. Current main password New main password Confirm new main password diff --git a/user-settings/presentation/src/test/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModelTest.kt b/user-settings/presentation/src/test/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModelTest.kt index 71f9ad685..43c946f10 100644 --- a/user-settings/presentation/src/test/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModelTest.kt +++ b/user-settings/presentation/src/test/kotlin/me/proton/core/usersettings/presentation/viewmodel/PasswordManagementViewModelTest.kt @@ -37,6 +37,7 @@ import me.proton.core.test.kotlin.CoroutinesTest import me.proton.core.test.kotlin.flowTest import me.proton.core.user.domain.entity.Type import me.proton.core.user.domain.entity.User +import me.proton.core.user.domain.usecase.ObserveUser import me.proton.core.usersettings.domain.entity.PasswordSetting import me.proton.core.usersettings.domain.entity.RecoverySetting import me.proton.core.usersettings.domain.entity.TwoFASetting @@ -60,6 +61,7 @@ import kotlin.test.assertTrue class PasswordManagementViewModelTest : ArchTest by ArchTest(), CoroutinesTest by CoroutinesTest() { // region mocks + private val observeUser = mockk() private val observeUserRecovery = mockk() private val observeUserSettings = mockk() private val observeUserRecoverySelfInitiated = mockk() @@ -109,6 +111,7 @@ class PasswordManagementViewModelTest : ArchTest by ArchTest(), CoroutinesTest b delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) // endregion @@ -118,12 +121,14 @@ class PasswordManagementViewModelTest : ArchTest by ArchTest(), CoroutinesTest b @Before fun beforeEveryTest() { coEvery { isAccountRecoveryResetEnabled.invoke(testUserId) } returns false + coEvery { observeUser.invoke(testUserId) } returns flowOf(testUser) coEvery { observeUserRecovery.invoke(testUserId) } returns flowOf(testUser.recovery) coEvery { observeUserSettings.invoke(testUserId) } returns flowOf(testUserSettingsResponse) coEvery { observeUserRecoverySelfInitiated.invoke(testUserId) } returns flowOf(false) viewModel = PasswordManagementViewModel( keyStoreCrypto, + observeUser, observeUserRecovery, observeUserSettings, observeUserRecoverySelfInitiated, diff --git a/user/data/src/androidTest/kotlin/me/proton/core/user/data/TestUsers.kt b/user/data/src/androidTest/kotlin/me/proton/core/user/data/TestUsers.kt index 0ce3fca59..127623164 100644 --- a/user/data/src/androidTest/kotlin/me/proton/core/user/data/TestUsers.kt +++ b/user/data/src/androidTest/kotlin/me/proton/core/user/data/TestUsers.kt @@ -45,6 +45,7 @@ object TestUsers { email = "user1@example.com", displayName = "user 1 name", keys = listOf(Key1.response, Key2Inactive.response), + flags = emptyMap(), type = Type.Proton.value ) @@ -93,6 +94,7 @@ object TestUsers { email = "user1@example.com", displayName = "user 1 name", keys = listOf(Key1.response), + flags = emptyMap(), type = Type.Proton.value ) diff --git a/user/data/src/androidTest/kotlin/me/proton/core/user/data/UserManagerPasswordTests.kt b/user/data/src/androidTest/kotlin/me/proton/core/user/data/UserManagerPasswordTests.kt index ebdc89cad..93468ccb7 100644 --- a/user/data/src/androidTest/kotlin/me/proton/core/user/data/UserManagerPasswordTests.kt +++ b/user/data/src/androidTest/kotlin/me/proton/core/user/data/UserManagerPasswordTests.kt @@ -230,7 +230,8 @@ class UserManagerPasswordTests { ), srpSession = "test-srp-session", auth = mockk(), - secondFactorProof = null + secondFactorProof = null, + encryptedSecret = null ) // THEN diff --git a/user/data/src/androidTest/kotlin/me/proton/core/user/data/repository/UserRepositoryImplTests.kt b/user/data/src/androidTest/kotlin/me/proton/core/user/data/repository/UserRepositoryImplTests.kt index 5f19626a2..aaee72a72 100644 --- a/user/data/src/androidTest/kotlin/me/proton/core/user/data/repository/UserRepositoryImplTests.kt +++ b/user/data/src/androidTest/kotlin/me/proton/core/user/data/repository/UserRepositoryImplTests.kt @@ -718,6 +718,7 @@ class UserRepositoryImplTests { delinquent = null, recovery = null, passphrase = null, + flags = emptyMap(), maxBaseSpace = null, maxDriveSpace = null, usedBaseSpace = null, diff --git a/user/data/src/main/kotlin/me/proton/core/user/data/UserManagerImpl.kt b/user/data/src/main/kotlin/me/proton/core/user/data/UserManagerImpl.kt index e7bb50b7d..7b34220eb 100644 --- a/user/data/src/main/kotlin/me/proton/core/user/data/UserManagerImpl.kt +++ b/user/data/src/main/kotlin/me/proton/core/user/data/UserManagerImpl.kt @@ -31,6 +31,7 @@ import me.proton.core.crypto.common.keystore.decrypt import me.proton.core.crypto.common.keystore.encrypt import me.proton.core.crypto.common.keystore.use import me.proton.core.crypto.common.pgp.Armored +import me.proton.core.crypto.common.pgp.Based64Encoded import me.proton.core.crypto.common.pgp.SignatureContext import me.proton.core.crypto.common.srp.Auth import me.proton.core.crypto.common.srp.SrpProofs @@ -153,9 +154,10 @@ class UserManagerImpl @Inject constructor( userId: UserId, newPassword: EncryptedString, secondFactorProof: SecondFactorProof?, - proofs: SrpProofs, - srpSession: String, - auth: Auth? + proofs: SrpProofs?, + srpSession: String?, + auth: Auth?, + encryptedSecret: Based64Encoded? ): Boolean { newPassword.decrypt(keyStore).toByteArray().use { decryptedNewPassword -> val keySalt = pgp.generateNewKeySalt() @@ -181,7 +183,8 @@ class UserManagerImpl @Inject constructor( secondFactorProof = secondFactorProof, auth = auth, keys = updatedKeys, - userKeys = updatedUserKeys + userKeys = updatedUserKeys, + encryptedSecret = encryptedSecret ) lock(userId) diff --git a/user/data/src/main/kotlin/me/proton/core/user/data/db/UserDatabase.kt b/user/data/src/main/kotlin/me/proton/core/user/data/db/UserDatabase.kt index 3a2dabb7d..a3a8ef48d 100644 --- a/user/data/src/main/kotlin/me/proton/core/user/data/db/UserDatabase.kt +++ b/user/data/src/main/kotlin/me/proton/core/user/data/db/UserDatabase.kt @@ -130,5 +130,19 @@ interface UserDatabase : Database, UserKeyDatabase { ) } } + + /** + * - Added UserEntity column: flags. + */ + val MIGRATION_6 = object : DatabaseMigration { + override fun migrate(database: SupportSQLiteDatabase) { + database.addTableColumn( + table = "UserEntity", + column = "flags", + type = "TEXT", + defaultValue = null + ) + } + } } } diff --git a/user/data/src/main/kotlin/me/proton/core/user/data/entity/UserEntity.kt b/user/data/src/main/kotlin/me/proton/core/user/data/entity/UserEntity.kt index 6ad1ca79c..e4b8c29a6 100644 --- a/user/data/src/main/kotlin/me/proton/core/user/data/entity/UserEntity.kt +++ b/user/data/src/main/kotlin/me/proton/core/user/data/entity/UserEntity.kt @@ -62,6 +62,7 @@ data class UserEntity( @Embedded(prefix = "recovery_") val recovery: UserRecoveryEntity?, val passphrase: EncryptedByteArray?, + val flags: Map?, val maxBaseSpace: Long?, val maxDriveSpace: Long?, val usedBaseSpace: Long?, diff --git a/user/data/src/main/kotlin/me/proton/core/user/data/extension/UserMapper.kt b/user/data/src/main/kotlin/me/proton/core/user/data/extension/UserMapper.kt index 985b64c63..9634562d4 100644 --- a/user/data/src/main/kotlin/me/proton/core/user/data/extension/UserMapper.kt +++ b/user/data/src/main/kotlin/me/proton/core/user/data/extension/UserMapper.kt @@ -51,6 +51,7 @@ fun UserResponse.toUser(): User { delinquent = Delinquent.map[delinquent], recovery = recovery?.toUserRecovery(), keys = keys.map { it.toUserKey(userId) }, + flags = flags.orEmpty(), maxBaseSpace = maxBaseSpace, maxDriveSpace = maxDriveSpace, usedBaseSpace = usedBaseSpace, @@ -78,6 +79,7 @@ internal fun User.toEntity(passphrase: EncryptedByteArray?) = UserEntity( delinquent = delinquent?.value, recovery = recovery?.toUserRecoveryEntity(), passphrase = passphrase, + flags = flags, maxBaseSpace = maxBaseSpace, maxDriveSpace = maxDriveSpace, usedBaseSpace = usedBaseSpace, @@ -103,6 +105,7 @@ internal fun UserEntity.toUser(keys: List) = User( delinquent = Delinquent.map[delinquent], recovery = recovery?.toUserRecovery(), keys = keys, + flags = flags.orEmpty(), maxBaseSpace = maxBaseSpace, maxDriveSpace = maxDriveSpace, usedBaseSpace = usedBaseSpace, diff --git a/user/data/src/test/kotlin/me/proton/core/user/data/UserManagerImplTest.kt b/user/data/src/test/kotlin/me/proton/core/user/data/UserManagerImplTest.kt index 1d8353958..22e19e36f 100644 --- a/user/data/src/test/kotlin/me/proton/core/user/data/UserManagerImplTest.kt +++ b/user/data/src/test/kotlin/me/proton/core/user/data/UserManagerImplTest.kt @@ -275,6 +275,7 @@ class UserManagerImplTest { proofs = mockk(), srpSession = "srp", auth = mockk(), + encryptedSecret = null ) // Then assertFalse(result) @@ -296,6 +297,7 @@ class UserManagerImplTest { proofs = mockk(), srpSession = "srp", auth = mockk(), + encryptedSecret = null ) // Then coVerify { @@ -325,6 +327,7 @@ class UserManagerImplTest { proofs = mockk(), srpSession = "srp", auth = mockk(), + encryptedSecret = null ) // Then coVerify { @@ -351,6 +354,7 @@ class UserManagerImplTest { proofs = mockk(), srpSession = "srp", auth = mockk(), + encryptedSecret = null ) // Then coVerify { diff --git a/user/domain/src/main/kotlin/me/proton/core/user/domain/UserManager.kt b/user/domain/src/main/kotlin/me/proton/core/user/domain/UserManager.kt index 4d9bf8ec8..9a8ae742c 100644 --- a/user/domain/src/main/kotlin/me/proton/core/user/domain/UserManager.kt +++ b/user/domain/src/main/kotlin/me/proton/core/user/domain/UserManager.kt @@ -138,9 +138,10 @@ interface UserManager { userId: UserId, newPassword: EncryptedString, secondFactorProof: SecondFactorProof?, - proofs: SrpProofs, - srpSession: String, - auth: Auth? + proofs: SrpProofs?, + srpSession: String?, + auth: Auth?, + encryptedSecret: Based64Encoded? ): Boolean /** diff --git a/user/domain/src/main/kotlin/me/proton/core/user/domain/entity/User.kt b/user/domain/src/main/kotlin/me/proton/core/user/domain/entity/User.kt index 09af6e08a..87f298b1e 100644 --- a/user/domain/src/main/kotlin/me/proton/core/user/domain/entity/User.kt +++ b/user/domain/src/main/kotlin/me/proton/core/user/domain/entity/User.kt @@ -99,6 +99,7 @@ data class User( * @see [UserManager.lock] * */ override val keys: List, + val flags: Map, val maxBaseSpace: Long? = null, val maxDriveSpace: Long? = null, val usedBaseSpace: Long? = null, diff --git a/user/domain/src/main/kotlin/me/proton/core/user/domain/extension/User.kt b/user/domain/src/main/kotlin/me/proton/core/user/domain/extension/User.kt index d60055819..4bf1e9910 100644 --- a/user/domain/src/main/kotlin/me/proton/core/user/domain/extension/User.kt +++ b/user/domain/src/main/kotlin/me/proton/core/user/domain/extension/User.kt @@ -95,10 +95,14 @@ fun User.getUsedDriveSpacePercentage(): Int? = getUsedPercentage(usedDriveSpace, fun User.getUsedTotalSpacePercentage(): Int = getUsedPercentage(usedSpace, maxSpace)!! /** - * @return true if the user have a temporary password. + * @return true if the user has a temporary password. */ -fun User.hasTemporaryPassword() = false +fun User.hasTemporaryPassword(): Boolean = flags.getOrDefault("has-temporary-password", false) +/** + * @return true if the user is SSO. + */ +fun User.isSso(): Boolean = flags.getOrDefault("sso", false) @Suppress("MagicNumber") private fun getUsedPercentage(used: Long?, max: Long?): Int? { diff --git a/user/domain/src/test/kotlin/me/proton/core/user/domain/extension/UserKtTest.kt b/user/domain/src/test/kotlin/me/proton/core/user/domain/extension/UserKtTest.kt index 76807f2e0..5e9d377f6 100644 --- a/user/domain/src/test/kotlin/me/proton/core/user/domain/extension/UserKtTest.kt +++ b/user/domain/src/test/kotlin/me/proton/core/user/domain/extension/UserKtTest.kt @@ -46,6 +46,7 @@ class UserKtTest { delinquent = null, recovery = null, keys = emptyList(), + flags = emptyMap(), type = Type.Proton ) diff --git a/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/SerializationUtils.kt b/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/SerializationUtils.kt index 88b972617..43b2b3b6a 100644 --- a/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/SerializationUtils.kt +++ b/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/SerializationUtils.kt @@ -97,6 +97,16 @@ inline fun String.deserializeList(): List = Serializer.deco inline fun String.deserializeMap(): Map = Serializer.decodeFromString(this) +/** + * @return [Map] of [T], [V] object from the receiver [String] + * This uses reflection: TODO improve for avoid it + */ +@NeedSerializable +inline fun String.deserializeMapOrNull(): Map? = try { + deserializeMap() +} catch (e: SerializationException) { + null +} /** * @return [String] from the receiver [T] object