feat(auth): wire changePassword with reauth + typed exceptions in FirebaseAuthDataSource

This commit is contained in:
null 2026-07-01 02:19:01 -05:00
parent 07eaf3c989
commit d1e23f24ee
1 changed files with 58 additions and 0 deletions

View File

@ -3,11 +3,15 @@ package app.closer.data.remote
import app.closer.BuildConfig import app.closer.BuildConfig
import app.closer.domain.model.AuthState import app.closer.domain.model.AuthState
import app.closer.domain.model.GoogleSignInResult import app.closer.domain.model.GoogleSignInResult
import app.closer.domain.repository.ChangePasswordException
import app.closer.domain.repository.PasswordResetException import app.closer.domain.repository.PasswordResetException
import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.FirebaseAuthInvalidUserException import com.google.firebase.auth.FirebaseAuthInvalidUserException
import com.google.firebase.FirebaseTooManyRequestsException
import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException
import com.google.firebase.auth.FirebaseAuthWeakPasswordException
import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.auth.GoogleAuthProvider
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -119,6 +123,60 @@ class FirebaseAuthDataSource @Inject constructor() {
.addOnFailureListener { cont.resumeWithException(it) } .addOnFailureListener { cont.resumeWithException(it) }
} }
/**
* Changes the signed-in user's password: re-authenticates with [currentPassword] (Firebase requires
* a recent login to change credentials), then sets [newPassword]. Firebase specifics are translated
* into typed [ChangePasswordException]s so the UI never string-matches error messages. Firebase's own
* server-side throttling ([FirebaseTooManyRequestsException]) guards against brute-forcing the current
* password; we surface it as [ChangePasswordException.TooManyAttempts].
*/
suspend fun changePassword(currentPassword: String, newPassword: String) {
val user = auth.currentUser ?: throw ChangePasswordException.ReauthRequired()
val email = user.email
val hasPasswordProvider = user.providerData.any { it.providerId == EmailAuthProvider.PROVIDER_ID }
if (email.isNullOrBlank() || !hasPasswordProvider) throw ChangePasswordException.NoPassword()
// 1) Prove it's really them with the current password.
try {
reauthenticate(user, EmailAuthProvider.getCredential(email, currentPassword))
} catch (e: FirebaseAuthInvalidCredentialsException) {
throw ChangePasswordException.WrongCurrentPassword()
} catch (e: FirebaseAuthInvalidUserException) {
throw ChangePasswordException.WrongCurrentPassword()
} catch (e: FirebaseTooManyRequestsException) {
throw ChangePasswordException.TooManyAttempts()
}
// 2) Set the new password.
try {
updatePassword(user, newPassword)
} catch (e: FirebaseAuthWeakPasswordException) {
throw ChangePasswordException.WeakNewPassword()
} catch (e: FirebaseAuthRecentLoginRequiredException) {
throw ChangePasswordException.ReauthRequired()
} catch (e: FirebaseTooManyRequestsException) {
throw ChangePasswordException.TooManyAttempts()
}
}
private suspend fun reauthenticate(
user: com.google.firebase.auth.FirebaseUser,
credential: com.google.firebase.auth.AuthCredential
): Unit = suspendCancellableCoroutine { cont ->
user.reauthenticate(credential)
.addOnSuccessListener { cont.resume(Unit) }
.addOnFailureListener { cont.resumeWithException(it) }
}
private suspend fun updatePassword(
user: com.google.firebase.auth.FirebaseUser,
newPassword: String
): Unit = suspendCancellableCoroutine { cont ->
user.updatePassword(newPassword)
.addOnSuccessListener { cont.resume(Unit) }
.addOnFailureListener { cont.resumeWithException(it) }
}
suspend fun signInWithGoogle(idToken: String): GoogleSignInResult = suspend fun signInWithGoogle(idToken: String): GoogleSignInResult =
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
val credential = GoogleAuthProvider.getCredential(idToken, null) val credential = GoogleAuthProvider.getCredential(idToken, null)