mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
parent
e1cad830e9
commit
389b990358
2 changed files with 94 additions and 7 deletions
|
|
@ -7,6 +7,7 @@ import com.nuvio.app.core.storage.LocalAccountDataCleaner
|
|||
import io.github.jan.supabase.auth.auth
|
||||
import io.github.jan.supabase.auth.providers.builtin.Email
|
||||
import io.github.jan.supabase.auth.status.SessionStatus
|
||||
import io.github.jan.supabase.exceptions.RestException
|
||||
import io.github.jan.supabase.functions.functions
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
|
@ -32,6 +33,7 @@ object AuthRepository {
|
|||
val error: StateFlow<String?> = _error.asStateFlow()
|
||||
|
||||
private var initialized = false
|
||||
private var validatedRemoteUserId: String? = null
|
||||
|
||||
fun initialize() {
|
||||
if (initialized) return
|
||||
|
|
@ -40,6 +42,7 @@ object AuthRepository {
|
|||
scope.launch {
|
||||
SyncBackendRepository.state.collectLatest { backendState ->
|
||||
if (!backendState.isLoaded) return@collectLatest
|
||||
validatedRemoteUserId = null
|
||||
|
||||
AuthStorage.loadAnonymousUserId()?.let { savedAnonId ->
|
||||
_state.value = AuthState.Authenticated(
|
||||
|
|
@ -56,8 +59,10 @@ object AuthRepository {
|
|||
when (status) {
|
||||
is SessionStatus.Authenticated -> {
|
||||
val user = status.session.user
|
||||
val userId = user?.id.orEmpty()
|
||||
if (!validateRemoteSession(userId)) return@collect
|
||||
_state.value = AuthState.Authenticated(
|
||||
userId = user?.id ?: "",
|
||||
userId = userId,
|
||||
email = user?.email,
|
||||
isAnonymous = false,
|
||||
)
|
||||
|
|
@ -79,6 +84,25 @@ object AuthRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun validateRemoteSession(userId: String): Boolean {
|
||||
if (userId.isBlank() || validatedRemoteUserId == userId) return true
|
||||
|
||||
return runCatching {
|
||||
SupabaseProvider.client.auth.retrieveUserForCurrentSession(false)
|
||||
validatedRemoteUserId = userId
|
||||
true
|
||||
}.getOrElse { e ->
|
||||
if (isInvalidRemoteSessionError(e)) {
|
||||
log.w(e) { "Stored Supabase session no longer belongs to an active account; clearing local auth" }
|
||||
clearLocalSessionAfterRemoteInvalidation()
|
||||
false
|
||||
} else {
|
||||
log.w(e) { "Unable to validate stored Supabase session; keeping cached auth state" }
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun signInAnonymously() {
|
||||
_error.value = null
|
||||
|
|
@ -118,6 +142,7 @@ object AuthRepository {
|
|||
_error.value = null
|
||||
val wasAnonymous = AuthStorage.loadAnonymousUserId() != null
|
||||
AuthStorage.clearAnonymousUserId()
|
||||
validatedRemoteUserId = null
|
||||
if (!wasAnonymous) {
|
||||
SupabaseProvider.client.auth.signOut()
|
||||
}
|
||||
|
|
@ -128,10 +153,32 @@ object AuthRepository {
|
|||
_error.value = e.message ?: getString(Res.string.auth_sign_out_failed)
|
||||
}
|
||||
|
||||
suspend fun signOutIfSessionInvalid(error: Throwable, source: String): Boolean {
|
||||
if (!isInvalidRemoteSessionError(error)) return false
|
||||
|
||||
log.w(error) { "$source failed because the current Supabase account/session is no longer valid; clearing local auth" }
|
||||
clearLocalSessionAfterRemoteInvalidation()
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun clearLocalSessionAfterRemoteInvalidation() {
|
||||
_error.value = null
|
||||
AuthStorage.clearAnonymousUserId()
|
||||
validatedRemoteUserId = null
|
||||
runCatching {
|
||||
SupabaseProvider.client.auth.clearSession()
|
||||
}.onFailure { e ->
|
||||
log.w(e) { "Failed to clear Supabase session after remote invalidation; continuing local reset" }
|
||||
}
|
||||
_state.value = AuthState.Unauthenticated
|
||||
LocalAccountDataCleaner.wipe()
|
||||
}
|
||||
|
||||
suspend fun resetForSyncBackendChange(): Result<Unit> = runCatching {
|
||||
_error.value = null
|
||||
val wasAnonymous = AuthStorage.loadAnonymousUserId() != null
|
||||
AuthStorage.clearAnonymousUserId()
|
||||
validatedRemoteUserId = null
|
||||
|
||||
if (!wasAnonymous) {
|
||||
runCatching {
|
||||
|
|
@ -152,6 +199,8 @@ object AuthRepository {
|
|||
_error.value = null
|
||||
SupabaseProvider.client.functions.invoke("delete-account")
|
||||
SupabaseProvider.client.auth.signOut()
|
||||
validatedRemoteUserId = null
|
||||
_state.value = AuthState.Unauthenticated
|
||||
LocalAccountDataCleaner.wipe()
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "Account deletion failed" }
|
||||
|
|
@ -161,4 +210,39 @@ object AuthRepository {
|
|||
fun clearError() {
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
private fun isInvalidRemoteSessionError(error: Throwable): Boolean {
|
||||
val restError = error.findCause<RestException>()
|
||||
if (restError?.statusCode == 401 || restError?.statusCode == 403) return true
|
||||
|
||||
val message = buildString {
|
||||
append(error.message.orEmpty())
|
||||
if (restError != null) {
|
||||
append(' ')
|
||||
append(restError.error)
|
||||
append(' ')
|
||||
append(restError.description)
|
||||
}
|
||||
}.lowercase()
|
||||
|
||||
return (
|
||||
"jwt" in message &&
|
||||
("invalid" in message || "expired" in message || "malformed" in message)
|
||||
) || (
|
||||
"user" in message &&
|
||||
("does not exist" in message || "not found" in message || "deleted" in message)
|
||||
) || (
|
||||
"foreign key" in message &&
|
||||
("auth.users" in message || "user_id" in message)
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T : Throwable> Throwable.findCause(): T? {
|
||||
var current: Throwable? = this
|
||||
while (current != null) {
|
||||
if (current is T) return current
|
||||
current = current.cause
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ object ProfileRepository {
|
|||
}
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
try {
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_profiles")
|
||||
val profiles = result.decodeList<NuvioProfile>()
|
||||
_state.value = _state.value.copy(
|
||||
|
|
@ -133,7 +133,8 @@ object ProfileRepository {
|
|||
activeProfileIndex = _state.value.activeProfile!!.profileIndex
|
||||
}
|
||||
persist()
|
||||
}.onFailure { e ->
|
||||
} catch (e: Throwable) {
|
||||
if (AuthRepository.signOutIfSessionInvalid(e, "Profile pull")) return
|
||||
log.e(e) { "Failed to pull profiles" }
|
||||
if (!_state.value.isLoaded) {
|
||||
_state.value = _state.value.copy(isLoaded = true)
|
||||
|
|
@ -182,13 +183,14 @@ object ProfileRepository {
|
|||
applyPayloadsLocally(profiles)
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
try {
|
||||
val params = buildJsonObject {
|
||||
put("p_profiles", json.encodeToJsonElement(profiles))
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_profiles", params)
|
||||
pullProfiles()
|
||||
}.onFailure { e ->
|
||||
} catch (e: Throwable) {
|
||||
if (AuthRepository.signOutIfSessionInvalid(e, "Profile push")) return
|
||||
log.e(e) { "Failed to push profiles" }
|
||||
}
|
||||
}
|
||||
|
|
@ -273,11 +275,12 @@ object ProfileRepository {
|
|||
persist()
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
try {
|
||||
val params = buildJsonObject { put("p_profile_id", profileIndex) }
|
||||
SupabaseProvider.client.postgrest.rpc("sync_delete_profile_data", params)
|
||||
pullProfiles()
|
||||
}.onFailure { e ->
|
||||
} catch (e: Throwable) {
|
||||
if (AuthRepository.signOutIfSessionInvalid(e, "Profile delete")) return
|
||||
log.e(e) { "Failed to delete profile $profileIndex" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue