Display a toast message when submitting invalid login credentials

This commit is contained in:
Z. Charles Dziura 2024-11-24 18:49:32 -05:00
parent 836e24247e
commit 78a4b75acd
11 changed files with 49 additions and 27 deletions

View file

@ -37,8 +37,8 @@ impl AppError {
Self::new(ErrorKind::ExpiredToken) Self::new(ErrorKind::ExpiredToken)
} }
pub fn invalid_password() -> Self { pub fn invalid_credentials() -> Self {
Self::new(ErrorKind::InvalidPassword) Self::new(ErrorKind::InvalidCredentials)
} }
pub fn invalid_token() -> Self { pub fn invalid_token() -> Self {
@ -68,6 +68,10 @@ impl AppError {
pub fn is_duplicate_record(&self) -> bool { pub fn is_duplicate_record(&self) -> bool {
matches!(self.kind, ErrorKind::DuplicateRecord(_)) matches!(self.kind, ErrorKind::DuplicateRecord(_))
} }
pub fn is_no_record_found(&self) -> bool {
matches!(self.kind, ErrorKind::NoDbRecordFound)
}
} }
impl From<ErrorKind> for AppError { impl From<ErrorKind> for AppError {
@ -131,7 +135,7 @@ impl Display for AppError {
write!(f, "Duplicate database record: {message}") write!(f, "Duplicate database record: {message}")
} }
ErrorKind::ExpiredToken => write!(f, "The provided token has expired"), 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::InvalidToken => write!(f, "The provided token is invalid"),
ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"), ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"),
ErrorKind::MissingEnvironmentVariables(missing_vars) => write!( ErrorKind::MissingEnvironmentVariables(missing_vars) => write!(
@ -164,7 +168,7 @@ enum ErrorKind {
DbMigration(MigrateError), DbMigration(MigrateError),
DuplicateRecord(String), DuplicateRecord(String),
ExpiredToken, ExpiredToken,
InvalidPassword, InvalidCredentials,
InvalidToken, InvalidToken,
MissingEnvironmentVariables(Vec<&'static str>), MissingEnvironmentVariables(Vec<&'static str>),
MissingSessionField(&'static str), MissingSessionField(&'static str),
@ -182,7 +186,7 @@ impl IntoResponse for AppError {
StatusCode::CONFLICT, StatusCode::CONFLICT,
ApiResponse::new_with_error(self).into_json_response(), ApiResponse::new_with_error(self).into_json_response(),
), ),
&ErrorKind::InvalidPassword | &ErrorKind::InvalidToken => ( &ErrorKind::InvalidCredentials | &ErrorKind::InvalidToken => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
ApiResponse::new_with_error(self).into_json_response(), ApiResponse::new_with_error(self).into_json_response(),
), ),

View file

@ -48,7 +48,15 @@ async fn auth_login_request(
let UserIdAndHashedPasswordEntity { let UserIdAndHashedPasswordEntity {
id: user_id, id: user_id,
password: hashed_password, 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)?; verify_password(password, hashed_password)?;

View file

@ -21,5 +21,5 @@ pub fn verify_password(password: String, hashed_password: String) -> Result<(),
algorithm algorithm
.verify_password(password.as_bytes(), &hash) .verify_password(password.as_bytes(), &hash)
.map_err(|_| AppError::invalid_password()) .map_err(|_| AppError::invalid_credentials())
} }

View file

@ -133,7 +133,7 @@ class MainActivity : ComponentActivity() {
} }
private fun handleNewUserVerificationIntent(userId: Int, verificationToken: String) { 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 { lifecycleScope.launch {
navigator.navigate(Destination.AuthUserConfirmation(userId, verificationToken)) navigator.navigate(Destination.AuthUserConfirmation(userId, verificationToken))

View file

@ -1,6 +1,8 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box 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( Scaffold(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {

View file

@ -1,5 +1,7 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.util.Log
import android.widget.Toast
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.domain.model.Token
import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +51,9 @@ class LoginScreenViewModel @Inject constructor(
private val _password = MutableStateFlow("") private val _password = MutableStateFlow("")
val password = _password.asStateFlow() val password = _password.asStateFlow()
private val _toastMessages = MutableSharedFlow<String>()
val toastMessages = _toastMessages.asSharedFlow()
fun onAction(action: LoginScreenStateAction) { fun onAction(action: LoginScreenStateAction) {
when (action) { when (action) {
is LoginScreenStateAction.UpdateEmailAddress -> { is LoginScreenStateAction.UpdateEmailAddress -> {
@ -87,8 +94,8 @@ class LoginScreenViewModel @Inject constructor(
private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) { private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) {
try { try {
val result = submitLoginCredentials(emailAddress, password) val result = submitLoginCredentials(emailAddress, password)
} catch (err: Throwable) { } catch (err: Exception) {
// TODO... _toastMessages.emit("Invalid Email Address or Password")
} }
} }

View file

@ -2,7 +2,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -73,7 +72,7 @@ fun RegistrationScreen(
if (result !is AccountManagerResult.Success) { if (result !is AccountManagerResult.Success) {
Log.i( Log.i(
"DebtPirate::RegistrationScreen", "RegistrationScreen",
"Not able to store credentials in CredentialManager" "Not able to store credentials in CredentialManager"
) )
} }

View file

@ -1,24 +1,15 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.usecase 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.AuthLoginPostRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
class SubmitLoginCredentialsUseCase( class SubmitLoginCredentialsUseCase(
private val authRepository: AuthRepository private val authRepository: AuthRepository
) { ) {
suspend operator fun invoke(emailAddress: String, password: String): AuthLoginPostResponse { suspend operator fun invoke(emailAddress: String, password: String): AuthLoginPostResponse {
val credentials = AuthLoginPostRequest(emailAddress, password) val credentials = AuthLoginPostRequest(emailAddress, password)
return authRepository.submitAuthLoginRequest(credentials)
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
}
} }
} }

View file

@ -1,3 +1,5 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.auth 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)

View file

@ -30,7 +30,7 @@ class AccountManager(private val context: Activity) {
AccountManagerResult.Success AccountManagerResult.Success
} catch (err: CreateCredentialNoCreateOptionException) { } catch (err: CreateCredentialNoCreateOptionException) {
Log.w( Log.w(
"DebtPirate::AccountManager", "AccountManager",
"Cannot store credentials; a Google account isn't associated with this device" "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) { } catch (err: CreateCredentialException) {
err.printStackTrace() err.printStackTrace()
Log.i( Log.i(
"DebtPirate::AccountManager", "AccountManager",
"Unable to store credentials: ${err.message}" "Unable to store credentials: ${err.message}"
) )

View file

@ -11,6 +11,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import retrofit2.Retrofit import retrofit2.Retrofit
class InvalidCredentialsException(): Exception("Invalid email address or password")
class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository { class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java) private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
@ -27,7 +29,7 @@ class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
val body = val body =
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType) gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
throw Throwable(body!!.error!!) throw InvalidCredentialsException()
} }
} }
} }