From 78a4b75acd60782bac6cff721edb23774ae0c0e7 Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Sun, 24 Nov 2024 18:49:32 -0500 Subject: [PATCH] Display a toast message when submitting invalid login credentials --- api/src/models/error.rs | 14 +++++++++----- api/src/requests/auth/login/handler.rs | 10 +++++++++- api/src/services/password_hasher.rs | 2 +- .../debtpirate/app/host/MainActivity.kt | 2 +- .../screen/auth/presentation/login/LoginScreen.kt | 9 +++++++++ .../presentation/login/LoginScreenViewModel.kt | 11 +++++++++-- .../presentation/register/RegistrationScreen.kt | 3 +-- .../auth/usecase/SubmitLoginCredentialsUseCase.kt | 13 ++----------- .../data/remote/model/auth/AuthLoginPostRequest.kt | 4 +++- .../debtpirate/domain/repository/AccountManager.kt | 4 ++-- .../domain/repository/AuthRepositoryImpl.kt | 4 +++- 11 files changed, 49 insertions(+), 27 deletions(-) diff --git a/api/src/models/error.rs b/api/src/models/error.rs index a3cc677..8e232ec 100644 --- a/api/src/models/error.rs +++ b/api/src/models/error.rs @@ -37,8 +37,8 @@ impl AppError { Self::new(ErrorKind::ExpiredToken) } - pub fn invalid_password() -> Self { - Self::new(ErrorKind::InvalidPassword) + pub fn invalid_credentials() -> Self { + Self::new(ErrorKind::InvalidCredentials) } pub fn invalid_token() -> Self { @@ -68,6 +68,10 @@ impl AppError { pub fn is_duplicate_record(&self) -> bool { matches!(self.kind, ErrorKind::DuplicateRecord(_)) } + + pub fn is_no_record_found(&self) -> bool { + matches!(self.kind, ErrorKind::NoDbRecordFound) + } } impl From for AppError { @@ -131,7 +135,7 @@ impl Display for AppError { write!(f, "Duplicate database record: {message}") } ErrorKind::ExpiredToken => write!(f, "The provided token has expired"), - ErrorKind::InvalidPassword => write!(f, "Invalid password"), + ErrorKind::InvalidCredentials => write!(f, "Invalid email address or password"), ErrorKind::InvalidToken => write!(f, "The provided token is invalid"), ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"), ErrorKind::MissingEnvironmentVariables(missing_vars) => write!( @@ -164,7 +168,7 @@ enum ErrorKind { DbMigration(MigrateError), DuplicateRecord(String), ExpiredToken, - InvalidPassword, + InvalidCredentials, InvalidToken, MissingEnvironmentVariables(Vec<&'static str>), MissingSessionField(&'static str), @@ -182,7 +186,7 @@ impl IntoResponse for AppError { StatusCode::CONFLICT, ApiResponse::new_with_error(self).into_json_response(), ), - &ErrorKind::InvalidPassword | &ErrorKind::InvalidToken => ( + &ErrorKind::InvalidCredentials | &ErrorKind::InvalidToken => ( StatusCode::BAD_REQUEST, ApiResponse::new_with_error(self).into_json_response(), ), diff --git a/api/src/requests/auth/login/handler.rs b/api/src/requests/auth/login/handler.rs index 5f757d1..cb310b4 100644 --- a/api/src/requests/auth/login/handler.rs +++ b/api/src/requests/auth/login/handler.rs @@ -48,7 +48,15 @@ async fn auth_login_request( let UserIdAndHashedPasswordEntity { id: user_id, password: hashed_password, - } = get_username_and_password_by_email(db_pool, email).await?; + } = get_username_and_password_by_email(db_pool, email) + .await + .map_err(|err| { + if err.is_no_record_found() { + AppError::invalid_credentials() + } else { + err + } + })?; verify_password(password, hashed_password)?; diff --git a/api/src/services/password_hasher.rs b/api/src/services/password_hasher.rs index 574204d..ec3252d 100644 --- a/api/src/services/password_hasher.rs +++ b/api/src/services/password_hasher.rs @@ -21,5 +21,5 @@ pub fn verify_password(password: String, hashed_password: String) -> Result<(), algorithm .verify_password(password.as_bytes(), &hash) - .map_err(|_| AppError::invalid_password()) + .map_err(|_| AppError::invalid_credentials()) } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt index 128a6d5..059baee 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt @@ -133,7 +133,7 @@ class MainActivity : ComponentActivity() { } private fun handleNewUserVerificationIntent(userId: Int, verificationToken: String) { - Log.d("DebtPirate::MainActivity", "User ID: $userId, Session Token: $verificationToken") + Log.d("MainActivity", "User ID: $userId, Session Token: $verificationToken") lifecycleScope.launch { navigator.navigate(Destination.AuthUserConfirmation(userId, verificationToken)) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt index b2038d1..08e398d 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt @@ -1,6 +1,8 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login import android.annotation.SuppressLint +import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -71,6 +73,13 @@ fun LoginScreen( } } + val toastMessages = viewModel.toastMessages.collectAsState("") + LaunchedEffect(toastMessages.value) { + val message = toastMessages.value + Log.d("LoginScreen", message) + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + Scaffold( modifier = Modifier.fillMaxSize() ) { diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt index 47ad9bd..41d4ea9 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt @@ -1,5 +1,7 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login +import android.util.Log +import android.widget.Toast import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -11,7 +13,9 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCred import ing.bikeshedengineer.debtpirate.domain.model.Token import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Navigator +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -47,6 +51,9 @@ class LoginScreenViewModel @Inject constructor( private val _password = MutableStateFlow("") val password = _password.asStateFlow() + private val _toastMessages = MutableSharedFlow() + val toastMessages = _toastMessages.asSharedFlow() + fun onAction(action: LoginScreenStateAction) { when (action) { is LoginScreenStateAction.UpdateEmailAddress -> { @@ -87,8 +94,8 @@ class LoginScreenViewModel @Inject constructor( private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) { try { val result = submitLoginCredentials(emailAddress, password) - } catch (err: Throwable) { - // TODO... + } catch (err: Exception) { + _toastMessages.emit("Invalid Email Address or Password") } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt index 9344371..25d3663 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt @@ -2,7 +2,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register import android.annotation.SuppressLint import android.util.Log -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -73,7 +72,7 @@ fun RegistrationScreen( if (result !is AccountManagerResult.Success) { Log.i( - "DebtPirate::RegistrationScreen", + "RegistrationScreen", "Not able to store credentials in CredentialManager" ) } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitLoginCredentialsUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitLoginCredentialsUseCase.kt index 18d025a..42bd34b 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitLoginCredentialsUseCase.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitLoginCredentialsUseCase.kt @@ -1,24 +1,15 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.usecase -import android.util.Log import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository + class SubmitLoginCredentialsUseCase( private val authRepository: AuthRepository ) { suspend operator fun invoke(emailAddress: String, password: String): AuthLoginPostResponse { val credentials = AuthLoginPostRequest(emailAddress, password) - - try { - val response = authRepository.submitAuthLoginRequest(credentials) - Log.d("AuthScreen", "Login successful! $response") - return response - } catch (err: Throwable) { - // TODO... - Log.e("AuthScreen", err.message!!) - throw err - } + return authRepository.submitAuthLoginRequest(credentials) } } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/auth/AuthLoginPostRequest.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/auth/AuthLoginPostRequest.kt index 2fefde4..73f8ec2 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/auth/AuthLoginPostRequest.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/auth/AuthLoginPostRequest.kt @@ -1,3 +1,5 @@ package ing.bikeshedengineer.debtpirate.data.remote.model.auth -data class AuthLoginPostRequest(val emailAddress: String, val password: String) +import com.google.gson.annotations.SerializedName + +data class AuthLoginPostRequest(@SerializedName("email") val emailAddress: String, val password: String) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt index f7bd7bb..0ebc15e 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt @@ -30,7 +30,7 @@ class AccountManager(private val context: Activity) { AccountManagerResult.Success } catch (err: CreateCredentialNoCreateOptionException) { Log.w( - "DebtPirate::AccountManager", + "AccountManager", "Cannot store credentials; a Google account isn't associated with this device" ) @@ -40,7 +40,7 @@ class AccountManager(private val context: Activity) { } catch (err: CreateCredentialException) { err.printStackTrace() Log.i( - "DebtPirate::AccountManager", + "AccountManager", "Unable to store credentials: ${err.message}" ) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AuthRepositoryImpl.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AuthRepositoryImpl.kt index c85cde5..54ab5c7 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AuthRepositoryImpl.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AuthRepositoryImpl.kt @@ -11,6 +11,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import retrofit2.Retrofit +class InvalidCredentialsException(): Exception("Invalid email address or password") + class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository { private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java) @@ -27,7 +29,7 @@ class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository { val body = gson.fromJson>(response.errorBody()!!.charStream(), errorType) - throw Throwable(body!!.error!!) + throw InvalidCredentialsException() } } }