diff --git a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt index 3d6c9cb2..88374ce0 100644 --- a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt @@ -3,11 +3,15 @@ package app.closer.data.remote import app.closer.BuildConfig import app.closer.domain.model.AuthState import app.closer.domain.model.GoogleSignInResult +import app.closer.domain.repository.ChangePasswordException import app.closer.domain.repository.PasswordResetException import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException 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 kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -119,6 +123,60 @@ class FirebaseAuthDataSource @Inject constructor() { .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 = suspendCancellableCoroutine { cont -> val credential = GoogleAuthProvider.getCredential(idToken, null)