Validate login credentials before submitting them

This commit is contained in:
Z. Charles Dziura 2024-12-15 10:15:29 -05:00
parent 5f6c3c1c8e
commit a1352f9cb6
9 changed files with 71 additions and 31 deletions

View file

@ -61,6 +61,10 @@ impl AppError {
Self::new(ErrorKind::NoSessionFound)
}
pub fn no_user_found() -> Self {
Self::new(ErrorKind::NoUserFound)
}
pub fn token_key() -> Self {
Self::new(ErrorKind::TokenKey)
}
@ -149,6 +153,7 @@ impl Display for AppError {
),
ErrorKind::NoDbRecordFound => write!(f, "No database record found"),
ErrorKind::NoSessionFound => write!(f, "No session found"),
ErrorKind::NoUserFound => write!(f, "No user found for that email address"),
ErrorKind::Sqlx(err) => write!(f, "{err}"),
ErrorKind::TokenKey => write!(
f,
@ -175,6 +180,7 @@ enum ErrorKind {
MissingAuthorizationToken,
NoDbRecordFound,
NoSessionFound,
NoUserFound,
Sqlx(SqlxError),
TokenKey,
}
@ -196,7 +202,7 @@ impl IntoResponse for AppError {
StatusCode::UNAUTHORIZED,
ApiResponse::new_with_error(self).into_json_response(),
),
&ErrorKind::NoDbRecordFound => (
&ErrorKind::NoDbRecordFound | &ErrorKind::NoUserFound => (
StatusCode::NOT_FOUND,
ApiResponse::new_with_error(self).into_json_response(),
),

View file

@ -52,7 +52,7 @@ async fn auth_login_request(
.await
.map_err(|err| {
if err.is_no_record_found() {
AppError::invalid_credentials()
AppError::no_user_found()
} else {
err
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -97,10 +98,13 @@ fun LoginScreen(
.padding(16.dp)
) {
val emailAddress = viewModel.emailAddress.collectAsState()
val isValidEmailAddress = viewModel.isValidEmailAddress.collectAsState(true)
val password = viewModel.password.collectAsState()
val isValidPassword = viewModel.isValidPassword.collectAsState(true)
LoginComponent(
emailAddress = emailAddress,
emailAddress,
onUpdateEmailAddress = {
viewModel.onAction(
LoginScreenStateAction.UpdateEmailAddress(
@ -108,7 +112,8 @@ fun LoginScreen(
)
)
},
password = password,
isValidEmailAddress,
password,
onUpdatePassword = {
viewModel.onAction(
LoginScreenStateAction.UpdatePassword(
@ -116,6 +121,7 @@ fun LoginScreen(
)
)
},
isValidPassword,
submitLoginRequest = {
viewModel.onAction(
LoginScreenStateAction.ValidateCredentials(
@ -155,8 +161,10 @@ private fun LoginScreenTopAppBar(modifier: Modifier = Modifier) {
private fun LoginComponent(
emailAddress: State<String>,
onUpdateEmailAddress: (String) -> Unit,
isValidEmailAddress: State<Boolean>,
password: State<String>,
onUpdatePassword: (String) -> Unit,
isValidPassword: State<Boolean>,
submitLoginRequest: () -> Unit,
modifier: Modifier = Modifier
) {
@ -173,6 +181,7 @@ private fun LoginComponent(
imeAction = ImeAction.Next
),
onValueChange = onUpdateEmailAddress,
isError = !isValidEmailAddress.value,
modifier = Modifier.fillMaxWidth()
)
@ -197,6 +206,7 @@ private fun LoginComponent(
onSend = { submitLoginRequest() }
),
onValueChange = onUpdatePassword,
isError = !isValidPassword.value,
modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(top = 4.dp))

View file

@ -1,12 +1,14 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.InvalidCredentialsException
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.LoginCredentialsValidationResult
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.UserNotFoundException
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.domain.repository.InvalidCredentialsException
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator
@ -14,6 +16,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -24,11 +28,19 @@ class LoginScreenViewModel @Inject constructor(
private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
private val updateStoreData: UpdateStoreDataUseCase
) : ViewModel() {
private val _isEmailAddressPristine = MutableStateFlow(true)
private val _emailAddress = MutableStateFlow("")
val emailAddress = _emailAddress.asStateFlow()
val isValidEmailAddress = _isEmailAddressPristine.combine(_emailAddress.map { it.isNotBlank() }) { isPristine, isValid ->
isPristine || isValid
}
private val _isPasswordPristine = MutableStateFlow(true)
private val _password = MutableStateFlow("")
val password = _password.asStateFlow()
val isValidPassword = _isPasswordPristine.combine(_password.map { it.isNotBlank() }) { isPristine, isValid ->
isPristine || isValid
}
private val _toastMessages = MutableSharedFlow<String>()
val toastMessages = _toastMessages.asSharedFlow()
@ -37,10 +49,12 @@ class LoginScreenViewModel @Inject constructor(
when (action) {
is LoginScreenStateAction.UpdateEmailAddress -> {
_emailAddress.value = action.emailAddress
_isEmailAddressPristine.value = false
}
is LoginScreenStateAction.UpdatePassword -> {
_password.value = action.password
_isPasswordPristine.value = false
}
is LoginScreenStateAction.ValidateCredentials -> {
@ -58,13 +72,18 @@ class LoginScreenViewModel @Inject constructor(
}
private fun onValidateLoginCredentials(emailAddress: String, password: String) {
when (validateLoginCredentials(emailAddress, password)) {
_isEmailAddressPristine.value = false
_isPasswordPristine.value = false
val validationResult = validateLoginCredentials(emailAddress, password)
when (validationResult) {
is LoginCredentialsValidationResult.ValidCredentials -> {
onAction(LoginScreenStateAction.SubmitLoginRequest(emailAddress, password))
}
else -> {
// TODO...
Log.d("LoginScreenViewModel", "Invalid credentials: $validationResult")
}
}
@ -86,6 +105,10 @@ class LoginScreenViewModel @Inject constructor(
_toastMessages.emit("Invalid Email Address or Password")
}
is UserNotFoundException -> {
_toastMessages.emit("User Not Found")
}
else -> {
_toastMessages.emit("Cannot Login, Please Try Again Later")
}

View file

@ -4,12 +4,26 @@ import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostReque
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
class InvalidCredentialsException() : Exception("Invalid email address or password")
class UserNotFoundException() : Exception("User account not found")
class SubmitLoginCredentialsUseCase(
private val authRepository: AuthRepository
) {
suspend operator fun invoke(emailAddress: String, password: String): AuthLoginPostResponse {
val credentials = AuthLoginPostRequest(emailAddress, password)
return authRepository.submitAuthLoginRequest(credentials)
val response = authRepository.submitAuthLoginRequest(credentials)
if (response.isSuccessful) {
val body = response.body()
return body!!.data!!
} else {
val statusCode = response.code()
if (statusCode == 404) {
throw UserNotFoundException()
} else {
throw InvalidCredentialsException()
}
}
}
}

View file

@ -1,10 +1,10 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.usecase
sealed class LoginCredentialsValidationResult {
object EmptyCredentials : LoginCredentialsValidationResult()
object EmptyEmailAddressField : LoginCredentialsValidationResult()
object EmptyPasswordField : LoginCredentialsValidationResult()
object ValidCredentials : LoginCredentialsValidationResult()
data object EmptyCredentials : LoginCredentialsValidationResult()
data object EmptyEmailAddressField : LoginCredentialsValidationResult()
data object EmptyPasswordField : LoginCredentialsValidationResult()
data object ValidCredentials : LoginCredentialsValidationResult()
}
class ValidateLoginCredentialsUseCase {

View file

@ -1,9 +1,10 @@
package ing.bikeshedengineer.debtpirate.data.repository
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
import retrofit2.Response
interface AuthRepository {
suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): AuthLoginPostResponse
suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>>
}

View file

@ -1,7 +1,5 @@
package ing.bikeshedengineer.debtpirate.domain.repository
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
@ -9,28 +7,16 @@ import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRespo
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Response
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)
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): AuthLoginPostResponse {
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>> {
return withContext(Dispatchers.IO) {
val response = authEndpoint.submitAuthLoginRequest(credentials)
if (response.isSuccessful) {
val body = response.body()
return@withContext body!!.data!!
} else {
val gson = Gson()
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
val body =
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
throw InvalidCredentialsException()
}
return@withContext response
}
}
}

View file

@ -1,6 +1,6 @@
[versions]
activityCompose = "1.9.3"
agp = "8.7.2"
agp = "8.7.3"
appcompat = "1.7.0"
composeBom = "2024.11.00"
coreKtx = "1.15.0"