Validate login credentials before submitting them
This commit is contained in:
parent
5f6c3c1c8e
commit
a1352f9cb6
9 changed files with 71 additions and 31 deletions
|
@ -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(),
|
||||
),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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>>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue