Skip to content

Commit b6f54f4

Browse files
authored
sync: smoother user repository handling (fixes #13584) (#13542)
1 parent 2909b57 commit b6f54f4

4 files changed

Lines changed: 198 additions & 184 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ android {
1212
applicationId "org.ole.planet.myplanet"
1313
minSdk = 26
1414
targetSdk = 36
15-
versionCode = 5583
16-
versionName = "0.55.83"
15+
versionCode = 5584
16+
versionName = "0.55.84"
1717
ndkVersion = '26.3.11579264'
1818
vectorDrawables.useSupportLibrary = true
1919
}

app/src/main/java/org/ole/planet/myplanet/repository/UserRepositoryImpl.kt

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import org.ole.planet.myplanet.utils.AndroidDecrypter
4141
import org.ole.planet.myplanet.utils.DispatcherProvider
4242
import org.ole.planet.myplanet.utils.JsonUtils
4343
import org.ole.planet.myplanet.utils.TimeUtils
44+
import android.util.Base64
45+
import java.io.IOException
46+
import java.util.Date
47+
import org.ole.planet.myplanet.utils.RetryUtils
48+
import org.ole.planet.myplanet.utils.SecurePrefs
49+
import org.ole.planet.myplanet.utils.Utilities
4450
import org.ole.planet.myplanet.utils.UrlUtils
4551

4652
class UserRepositoryImpl @Inject constructor(
@@ -682,7 +688,7 @@ class UserRepositoryImpl @Inject constructor(
682688

683689
if (userModel != null) {
684690
try {
685-
uploadToShelfService.get().saveKeyIv(apiInterface, userModel, obj)
691+
saveKeyIv(userModel, obj)
686692
} catch (_: Exception) { }
687693
Result.success(userModel)
688694
} else {
@@ -694,6 +700,183 @@ class UserRepositoryImpl @Inject constructor(
694700
}
695701
}
696702

703+
704+
override suspend fun changeUserSecurity(model: RealmUser, obj: JsonObject) {
705+
val table = "userdb-${Utilities.toHex(model.planetCode)}-${Utilities.toHex(model.name)}"
706+
val header = "Basic ${Base64.encodeToString(("${obj["name"].asString}:${obj["password"].asString}").toByteArray(), Base64.NO_WRAP)}"
707+
try {
708+
val response = apiInterface.getJsonObject(header, "${UrlUtils.getUrl()}/${table}/_security")
709+
if (response.body() != null) {
710+
val jsonObject = response.body()
711+
val members = jsonObject?.getAsJsonObject("members")
712+
val rolesArray: JsonArray = if (members?.has("roles") == true) {
713+
members.getAsJsonArray("roles")
714+
} else {
715+
JsonArray()
716+
}
717+
rolesArray.add("health")
718+
members?.add("roles", rolesArray)
719+
jsonObject?.add("members", members)
720+
apiInterface.putDoc(header, "application/json", "${UrlUtils.getUrl()}/${table}/_security", jsonObject)
721+
}
722+
} catch (e: Exception) {
723+
e.printStackTrace()
724+
}
725+
}
726+
727+
override suspend fun saveKeyIv(model: RealmUser, obj: JsonObject) {
728+
val table = "userdb-${Utilities.toHex(model.planetCode)}-${Utilities.toHex(model.name)}"
729+
val header = "Basic ${Base64.encodeToString(("${obj["name"].asString}:${obj["password"].asString}").toByteArray(), Base64.NO_WRAP)}"
730+
val ob = JsonObject()
731+
var keyString = AndroidDecrypter.generateKey()
732+
var iv: String? = AndroidDecrypter.generateIv()
733+
734+
if (!TextUtils.isEmpty(model.iv)) {
735+
iv = model.iv
736+
}
737+
if (!TextUtils.isEmpty(model.key)) {
738+
keyString = model.key
739+
}
740+
741+
ob.addProperty("key", keyString)
742+
ob.addProperty("iv", iv)
743+
ob.addProperty("createdOn", Date().time)
744+
745+
val maxAttempts = 3
746+
val retryDelayMs = 2000L
747+
val dbUrl = "${UrlUtils.getUrl()}/$table"
748+
749+
withContext(dispatcherProvider.io) {
750+
try {
751+
apiInterface.putDoc(header, "application/json", dbUrl, JsonObject())
752+
} catch (e: Exception) {
753+
null
754+
}
755+
}
756+
757+
val response = withContext(dispatcherProvider.io) {
758+
RetryUtils.retry(
759+
maxAttempts = maxAttempts,
760+
delayMs = retryDelayMs,
761+
shouldRetry = { resp -> resp == null || !resp.isSuccessful || resp.body() == null }
762+
) {
763+
apiInterface.postDoc(header, "application/json", "${UrlUtils.getUrl()}/$table", ob)
764+
}
765+
}
766+
767+
if (response?.isSuccessful == true && response.body() != null) {
768+
changeUserSecurity(model, obj)
769+
770+
markUserKeyIvSaved(model.id ?: "", keyString ?: "", iv)
771+
} else {
772+
throw IOException("Failed to save key/IV after $maxAttempts attempts")
773+
}
774+
}
775+
776+
private fun replacedUrl(model: RealmUser): String {
777+
val url = UrlUtils.getUrl()
778+
val password = SecurePrefs.getPassword(context, settings) ?: ""
779+
val replacedUrl = url.replaceFirst("[^:]+:[^@]+@".toRegex(), "${model.name}:${password}@")
780+
val protocolIndex = url.indexOf("://")
781+
val protocol = url.substring(0, protocolIndex)
782+
return "$protocol://$replacedUrl"
783+
}
784+
785+
override suspend fun checkIfUserExists(header: String, model: RealmUser): Boolean {
786+
try {
787+
val res = apiInterface.getJsonObject(header, "${replacedUrl(model)}/_users/org.couchdb.user:${model.name}")
788+
val exists = res.body() != null
789+
return exists
790+
} catch (e: Exception) {
791+
e.printStackTrace()
792+
return false
793+
}
794+
}
795+
796+
override suspend fun processUserAfterCreation(model: RealmUser, obj: JsonObject, updateHealthFn: suspend (String, String) -> Unit) {
797+
try {
798+
val password = model.password ?: SecurePrefs.getPassword(context, settings) ?: ""
799+
val header = "Basic ${Base64.encodeToString(("${model.name}:${password}").toByteArray(), Base64.NO_WRAP)}"
800+
val fetchDataResponse = apiInterface.getJsonObject(header, "${replacedUrl(model)}/_users/${model._id}")
801+
802+
if (fetchDataResponse.isSuccessful) {
803+
val passwordScheme = JsonUtils.getString("password_scheme", fetchDataResponse.body())
804+
val derivedKey = JsonUtils.getString("derived_key", fetchDataResponse.body())
805+
val salt = JsonUtils.getString("salt", fetchDataResponse.body())
806+
val iterations = JsonUtils.getString("iterations", fetchDataResponse.body())
807+
808+
model.password_scheme = passwordScheme
809+
model.derived_key = derivedKey
810+
model.salt = salt
811+
model.iterations = iterations
812+
813+
updateSecurityData(
814+
model.name ?: "",
815+
model._id,
816+
model._rev,
817+
derivedKey,
818+
salt,
819+
passwordScheme,
820+
iterations
821+
)
822+
823+
saveKeyIv(model, obj)
824+
825+
updateHealthFn(model.id ?: "", model._id ?: "")
826+
}
827+
} catch (e: Exception) {
828+
e.printStackTrace()
829+
}
830+
}
831+
832+
override suspend fun uploadNewUser(model: RealmUser, updateHealthFn: suspend (String, String) -> Unit) {
833+
try {
834+
val obj = model.serialize()
835+
val createResponse = apiInterface.putDoc(null, "application/json", "${replacedUrl(model)}/_users/org.couchdb.user:${model.name}", obj)
836+
837+
if (createResponse.isSuccessful) {
838+
val id = createResponse.body()?.get("id")?.asString
839+
val rev = createResponse.body()?.get("rev")?.asString
840+
model._id = id
841+
model._rev = rev
842+
843+
// Persist _id and _rev to database
844+
markUserUploaded(model.id ?: "", id ?: "", rev ?: "")
845+
846+
processUserAfterCreation(model, obj, updateHealthFn)
847+
}
848+
} catch (e: Exception) {
849+
e.printStackTrace()
850+
}
851+
}
852+
853+
override suspend fun updateExistingUser(header: String, model: RealmUser) {
854+
try {
855+
val latestDocResponse = apiInterface.getJsonObject(header, "${replacedUrl(model)}/_users/org.couchdb.user:${model.name}")
856+
857+
if (latestDocResponse.isSuccessful) {
858+
val latestRev = latestDocResponse.body()?.get("_rev")?.asString
859+
val obj = model.serialize()
860+
val objMap = obj.entrySet().associate { (key, value) -> key to value }
861+
val mutableObj = mutableMapOf<String, Any>().apply { putAll(objMap) }
862+
latestRev?.let { rev -> mutableObj["_rev"] = rev as Any }
863+
864+
val gson = com.google.gson.Gson()
865+
val jsonElement = gson.toJsonTree(mutableObj)
866+
val jsonObject = jsonElement.asJsonObject
867+
868+
val updateResponse = apiInterface.putDoc(header, "application/json", "${replacedUrl(model)}/_users/org.couchdb.user:${model.name}", jsonObject)
869+
870+
if (updateResponse.isSuccessful) {
871+
val updatedRev = updateResponse.body()?.get("rev")?.asString
872+
markUserRevUpdated(model.id ?: "", updatedRev)
873+
}
874+
}
875+
} catch (e: Exception) {
876+
e.printStackTrace()
877+
}
878+
}
879+
697880
override suspend fun getActiveUserIdSuspending(): String {
698881
return getUserModelSuspending()?.id ?: ""
699882
}

app/src/main/java/org/ole/planet/myplanet/repository/UserSyncRepository.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import com.google.gson.JsonObject
55
import org.ole.planet.myplanet.model.RealmUser
66

77
interface UserSyncRepository {
8+
suspend fun changeUserSecurity(model: RealmUser, obj: JsonObject)
9+
suspend fun saveKeyIv(model: RealmUser, obj: JsonObject)
10+
suspend fun checkIfUserExists(header: String, model: RealmUser): Boolean
11+
suspend fun processUserAfterCreation(model: RealmUser, obj: JsonObject, updateHealthFn: suspend (String, String) -> Unit)
12+
suspend fun uploadNewUser(model: RealmUser, updateHealthFn: suspend (String, String) -> Unit)
13+
suspend fun updateExistingUser(header: String, model: RealmUser)
814
suspend fun saveUser(jsonDoc: JsonObject?, key: String? = null, iv: String? = null): RealmUser?
915
fun bulkInsertAchievementsFromSync(realm: io.realm.Realm, jsonArray: JsonArray)
1016
fun bulkInsertUsersFromSync(realm: io.realm.Realm, jsonArray: JsonArray)

0 commit comments

Comments
 (0)