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)
|
Self::new(ErrorKind::NoSessionFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn no_user_found() -> Self {
|
||||||
|
Self::new(ErrorKind::NoUserFound)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn token_key() -> Self {
|
pub fn token_key() -> Self {
|
||||||
Self::new(ErrorKind::TokenKey)
|
Self::new(ErrorKind::TokenKey)
|
||||||
}
|
}
|
||||||
|
@ -149,6 +153,7 @@ impl Display for AppError {
|
||||||
),
|
),
|
||||||
ErrorKind::NoDbRecordFound => write!(f, "No database record found"),
|
ErrorKind::NoDbRecordFound => write!(f, "No database record found"),
|
||||||
ErrorKind::NoSessionFound => write!(f, "No session 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::Sqlx(err) => write!(f, "{err}"),
|
||||||
ErrorKind::TokenKey => write!(
|
ErrorKind::TokenKey => write!(
|
||||||
f,
|
f,
|
||||||
|
@ -175,6 +180,7 @@ enum ErrorKind {
|
||||||
MissingAuthorizationToken,
|
MissingAuthorizationToken,
|
||||||
NoDbRecordFound,
|
NoDbRecordFound,
|
||||||
NoSessionFound,
|
NoSessionFound,
|
||||||
|
NoUserFound,
|
||||||
Sqlx(SqlxError),
|
Sqlx(SqlxError),
|
||||||
TokenKey,
|
TokenKey,
|
||||||
}
|
}
|
||||||
|
@ -196,7 +202,7 @@ impl IntoResponse for AppError {
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
ApiResponse::new_with_error(self).into_json_response(),
|
ApiResponse::new_with_error(self).into_json_response(),
|
||||||
),
|
),
|
||||||
&ErrorKind::NoDbRecordFound => (
|
&ErrorKind::NoDbRecordFound | &ErrorKind::NoUserFound => (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
ApiResponse::new_with_error(self).into_json_response(),
|
ApiResponse::new_with_error(self).into_json_response(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -52,7 +52,7 @@ async fn auth_login_request(
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
if err.is_no_record_found() {
|
if err.is_no_record_found() {
|
||||||
AppError::invalid_credentials()
|
AppError::no_user_found()
|
||||||
} else {
|
} else {
|
||||||
err
|
err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||||
import android.widget.Toast
|
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.border
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
@ -97,10 +98,13 @@ fun LoginScreen(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
val emailAddress = viewModel.emailAddress.collectAsState()
|
val emailAddress = viewModel.emailAddress.collectAsState()
|
||||||
|
val isValidEmailAddress = viewModel.isValidEmailAddress.collectAsState(true)
|
||||||
|
|
||||||
val password = viewModel.password.collectAsState()
|
val password = viewModel.password.collectAsState()
|
||||||
|
val isValidPassword = viewModel.isValidPassword.collectAsState(true)
|
||||||
|
|
||||||
LoginComponent(
|
LoginComponent(
|
||||||
emailAddress = emailAddress,
|
emailAddress,
|
||||||
onUpdateEmailAddress = {
|
onUpdateEmailAddress = {
|
||||||
viewModel.onAction(
|
viewModel.onAction(
|
||||||
LoginScreenStateAction.UpdateEmailAddress(
|
LoginScreenStateAction.UpdateEmailAddress(
|
||||||
|
@ -108,7 +112,8 @@ fun LoginScreen(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
password = password,
|
isValidEmailAddress,
|
||||||
|
password,
|
||||||
onUpdatePassword = {
|
onUpdatePassword = {
|
||||||
viewModel.onAction(
|
viewModel.onAction(
|
||||||
LoginScreenStateAction.UpdatePassword(
|
LoginScreenStateAction.UpdatePassword(
|
||||||
|
@ -116,6 +121,7 @@ fun LoginScreen(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
isValidPassword,
|
||||||
submitLoginRequest = {
|
submitLoginRequest = {
|
||||||
viewModel.onAction(
|
viewModel.onAction(
|
||||||
LoginScreenStateAction.ValidateCredentials(
|
LoginScreenStateAction.ValidateCredentials(
|
||||||
|
@ -155,8 +161,10 @@ private fun LoginScreenTopAppBar(modifier: Modifier = Modifier) {
|
||||||
private fun LoginComponent(
|
private fun LoginComponent(
|
||||||
emailAddress: State<String>,
|
emailAddress: State<String>,
|
||||||
onUpdateEmailAddress: (String) -> Unit,
|
onUpdateEmailAddress: (String) -> Unit,
|
||||||
|
isValidEmailAddress: State<Boolean>,
|
||||||
password: State<String>,
|
password: State<String>,
|
||||||
onUpdatePassword: (String) -> Unit,
|
onUpdatePassword: (String) -> Unit,
|
||||||
|
isValidPassword: State<Boolean>,
|
||||||
submitLoginRequest: () -> Unit,
|
submitLoginRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
@ -173,6 +181,7 @@ private fun LoginComponent(
|
||||||
imeAction = ImeAction.Next
|
imeAction = ImeAction.Next
|
||||||
),
|
),
|
||||||
onValueChange = onUpdateEmailAddress,
|
onValueChange = onUpdateEmailAddress,
|
||||||
|
isError = !isValidEmailAddress.value,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -197,6 +206,7 @@ private fun LoginComponent(
|
||||||
onSend = { submitLoginRequest() }
|
onSend = { submitLoginRequest() }
|
||||||
),
|
),
|
||||||
onValueChange = onUpdatePassword,
|
onValueChange = onUpdatePassword,
|
||||||
|
isError = !isValidPassword.value,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(PaddingValues(top = 4.dp))
|
.padding(PaddingValues(top = 4.dp))
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
|
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.LoginCredentialsValidationResult
|
||||||
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase
|
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.app.screen.auth.usecase.ValidateLoginCredentialsUseCase
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.InvalidCredentialsException
|
|
||||||
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
|
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
|
||||||
import ing.bikeshedengineer.debtpirate.navigation.Destination
|
import ing.bikeshedengineer.debtpirate.navigation.Destination
|
||||||
import ing.bikeshedengineer.debtpirate.navigation.Navigator
|
import ing.bikeshedengineer.debtpirate.navigation.Navigator
|
||||||
|
@ -14,6 +16,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -24,11 +28,19 @@ class LoginScreenViewModel @Inject constructor(
|
||||||
private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
|
private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
|
||||||
private val updateStoreData: UpdateStoreDataUseCase
|
private val updateStoreData: UpdateStoreDataUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val _isEmailAddressPristine = MutableStateFlow(true)
|
||||||
private val _emailAddress = MutableStateFlow("")
|
private val _emailAddress = MutableStateFlow("")
|
||||||
val emailAddress = _emailAddress.asStateFlow()
|
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("")
|
private val _password = MutableStateFlow("")
|
||||||
val password = _password.asStateFlow()
|
val password = _password.asStateFlow()
|
||||||
|
val isValidPassword = _isPasswordPristine.combine(_password.map { it.isNotBlank() }) { isPristine, isValid ->
|
||||||
|
isPristine || isValid
|
||||||
|
}
|
||||||
|
|
||||||
private val _toastMessages = MutableSharedFlow<String>()
|
private val _toastMessages = MutableSharedFlow<String>()
|
||||||
val toastMessages = _toastMessages.asSharedFlow()
|
val toastMessages = _toastMessages.asSharedFlow()
|
||||||
|
@ -37,10 +49,12 @@ class LoginScreenViewModel @Inject constructor(
|
||||||
when (action) {
|
when (action) {
|
||||||
is LoginScreenStateAction.UpdateEmailAddress -> {
|
is LoginScreenStateAction.UpdateEmailAddress -> {
|
||||||
_emailAddress.value = action.emailAddress
|
_emailAddress.value = action.emailAddress
|
||||||
|
_isEmailAddressPristine.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoginScreenStateAction.UpdatePassword -> {
|
is LoginScreenStateAction.UpdatePassword -> {
|
||||||
_password.value = action.password
|
_password.value = action.password
|
||||||
|
_isPasswordPristine.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoginScreenStateAction.ValidateCredentials -> {
|
is LoginScreenStateAction.ValidateCredentials -> {
|
||||||
|
@ -58,13 +72,18 @@ class LoginScreenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onValidateLoginCredentials(emailAddress: String, password: String) {
|
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 -> {
|
is LoginCredentialsValidationResult.ValidCredentials -> {
|
||||||
onAction(LoginScreenStateAction.SubmitLoginRequest(emailAddress, password))
|
onAction(LoginScreenStateAction.SubmitLoginRequest(emailAddress, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// TODO...
|
// TODO...
|
||||||
|
Log.d("LoginScreenViewModel", "Invalid credentials: $validationResult")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +105,10 @@ class LoginScreenViewModel @Inject constructor(
|
||||||
_toastMessages.emit("Invalid Email Address or Password")
|
_toastMessages.emit("Invalid Email Address or Password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is UserNotFoundException -> {
|
||||||
|
_toastMessages.emit("User Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
_toastMessages.emit("Cannot Login, Please Try Again Later")
|
_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.remote.model.auth.AuthLoginPostResponse
|
||||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||||
|
|
||||||
|
class InvalidCredentialsException() : Exception("Invalid email address or password")
|
||||||
|
class UserNotFoundException() : Exception("User account not found")
|
||||||
|
|
||||||
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)
|
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
|
package ing.bikeshedengineer.debtpirate.app.screen.auth.usecase
|
||||||
|
|
||||||
sealed class LoginCredentialsValidationResult {
|
sealed class LoginCredentialsValidationResult {
|
||||||
object EmptyCredentials : LoginCredentialsValidationResult()
|
data object EmptyCredentials : LoginCredentialsValidationResult()
|
||||||
object EmptyEmailAddressField : LoginCredentialsValidationResult()
|
data object EmptyEmailAddressField : LoginCredentialsValidationResult()
|
||||||
object EmptyPasswordField : LoginCredentialsValidationResult()
|
data object EmptyPasswordField : LoginCredentialsValidationResult()
|
||||||
object ValidCredentials : LoginCredentialsValidationResult()
|
data object ValidCredentials : LoginCredentialsValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ValidateLoginCredentialsUseCase {
|
class ValidateLoginCredentialsUseCase {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.repository
|
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.AuthLoginPostRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
interface AuthRepository {
|
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
|
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.ApiResponse
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
|
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
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 ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import retrofit2.Response
|
||||||
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)
|
||||||
|
|
||||||
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): AuthLoginPostResponse {
|
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val response = authEndpoint.submitAuthLoginRequest(credentials)
|
val response = authEndpoint.submitAuthLoginRequest(credentials)
|
||||||
|
return@withContext response
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.9.3"
|
||||||
agp = "8.7.2"
|
agp = "8.7.3"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
composeBom = "2024.11.00"
|
composeBom = "2024.11.00"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue