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)
}
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<ErrorKind> 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(),
),

View file

@ -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)?;

View file

@ -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())
}

View file

@ -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))

View file

@ -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()
) {

View file

@ -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<String>()
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")
}
}

View file

@ -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"
)
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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}"
)

View file

@ -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<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
throw Throwable(body!!.error!!)
throw InvalidCredentialsException()
}
}
}