-add a password and login table

-disallow screenshots
This commit is contained in:
m-kuchergin 2026-02-25 14:19:49 +03:00
parent 8540a15e54
commit aa78bfc14b
13 changed files with 106 additions and 50 deletions

View File

@ -4,7 +4,8 @@ object TestIds {
object Auth { object Auth {
const val ERROR = "auth_error" const val ERROR = "auth_error"
const val SIGN_BUTTON = "auth_sign_button" const val SIGN_BUTTON = "auth_sign_button"
const val CODE_INPUT = "auth_code_input" const val LOGIN_INPUT = "auth_login_input"
const val PASSWORD_INPUT = "auth_password_input"
} }
object Main { object Main {
const val ERROR = "main_error" const val ERROR = "main_error"

View File

@ -16,13 +16,13 @@ object AuthRepository {
private var codeCache: String? = null private var codeCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> { suspend fun checkAndSave(textLogin: String, textPassword: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success -> return NetworkDataSource.checkAuth(textLogin, textPassword).onSuccess { success ->
if (success) { if (success) {
codeCache = text codeCache = textLogin // TODO(переделать под отправку и логина и пароля в api)
App.context.userDataStore.edit { preferences -> App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(CODE_KEY) val prefKey = stringPreferencesKey(CODE_KEY)
preferences[prefKey] = text preferences[prefKey] = textLogin // TODO(переделать под отправку и логина и пароля в api)
} }
} }
} }

View File

@ -36,9 +36,9 @@ object NetworkDataSource {
} }
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(login: String, password: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.post(getUrl(login, Constants.AUTH_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> false else -> false

View File

@ -1,15 +0,0 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(
text: String
): Result<Unit> {
return repository.checkAndSave(text).mapCatching { success ->
if (!success) error("Code is incorrect")
}
}
}

View File

@ -0,0 +1,16 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthDataUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(
textLogin: String,
textPassword: String
): Result<Unit> {
return repository.checkAndSave(textLogin, textPassword).mapCatching { success ->
if (!success) error("Login or password is incorrect")
}
}
}

View File

@ -1,12 +0,0 @@
package ru.myitschool.work.domain.auth
class CheckCodeFormatUseCase {
operator fun invoke(
text: String
): Boolean {
return text.length == 4 && text.all { char ->
char.isLetterOrDigit() &&
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
}
}
}

View File

@ -0,0 +1,14 @@
package ru.myitschool.work.domain.auth
class CheckLoginFormatUseCase {
operator fun invoke(
text: String
): Boolean {
return text.all { char ->
(char.isLetterOrDigit() &&
((char in ('A'..'Z')) || (char in ('a'..'z')) || char.isDigit()))
|| char == '.'
}
}
}

View File

@ -0,0 +1,24 @@
package ru.myitschool.work.domain.auth
import java.util.Locale
import java.util.Locale.getDefault
class CheckPasswordFormatUseCase {
operator fun invoke(
textLogin: String,
textPassword: String
): Boolean {
val lowerCasePassword = textPassword.lowercase(getDefault())
val lowerCaseLogin = textLogin.lowercase(getDefault())
val intersect = lowerCasePassword.toList().intersect(lowerCaseLogin.toList());
return !(textPassword.length < 8 || !textPassword.all { char ->
char.isLetterOrDigit()
&& ((char in 'A'..'Z')
|| (char in 'a'..'z')
|| char.isDigit())
&& textPassword.count { it == char } < 3
&& intersect.size < 3
})
}
}

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.ui.root package ru.myitschool.work.ui.root
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -15,6 +16,8 @@ class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
setContent { setContent {
WorkTheme { WorkTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

View File

@ -1,6 +1,6 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
sealed interface AuthIntent { sealed interface AuthIntent {
data class Send(val text: String): AuthIntent data class Send(val textLogin: String, val textPassword: String): AuthIntent
data class TextInput(val text: String): AuthIntent data class TextInput(val textLogin: String, val textPassword: String): AuthIntent
} }

View File

@ -77,22 +77,39 @@ private fun Content(
viewModel: AuthViewModel, viewModel: AuthViewModel,
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by rememberSaveable { mutableStateOf("") } var inputLogin by rememberSaveable { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
TextField( TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(),
value = inputText, value = inputLogin,
onValueChange = { onValueChange = {
inputText = it inputLogin = it
viewModel.onIntent(AuthIntent.TextInput(it)) viewModel.onIntent(AuthIntent.TextInput(
it,
textPassword = ""
))
}, },
label = { Text(stringResource(R.string.auth_label)) } label = { Text(stringResource(R.string.auth_label_login)) }
)
var inputPassword by rememberSaveable { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
TextField(
modifier = Modifier.testTag(TestIds.Auth.PASSWORD_INPUT).fillMaxWidth(),
value = inputPassword,
onValueChange = {
inputPassword = it
viewModel.onIntent(AuthIntent.TextInput(
inputLogin,
textPassword = it
))
},
label = { Text(stringResource(R.string.auth_label_password)) }
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(inputLogin, inputPassword))
}, },
enabled = state.isEnabledSend enabled = state.isEnabledSend
) { ) {

View File

@ -10,13 +10,17 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase import ru.myitschool.work.domain.auth.CheckAndSaveAuthDataUseCase
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase import ru.myitschool.work.domain.auth.CheckLoginFormatUseCase
import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
class AuthViewModel : ViewModel() { class AuthViewModel : ViewModel() {
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() } private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } private val checkAndSaveAuthDataUseCase by lazy { CheckAndSaveAuthDataUseCase(AuthRepository) }
private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
private val _uiState = MutableStateFlow<AuthState>( private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data( AuthState.Data(
isEnabledSend = false, isEnabledSend = false,
@ -32,7 +36,7 @@ class AuthViewModel : ViewModel() {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
viewModelScope.launch { viewModelScope.launch {
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( checkAndSaveAuthDataUseCase.invoke(intent.textLogin, intent.textPassword).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(AuthAction.Open(MainScreenDestination)) _actionFlow.emit(AuthAction.Open(MainScreenDestination))
}, },
@ -46,10 +50,13 @@ class AuthViewModel : ViewModel() {
) )
} }
} }
is AuthIntent.TextInput -> { is AuthIntent.TextInput -> {
updateStateIfData { oldState -> updateStateIfData { oldState ->
oldState.copy( oldState.copy(
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text), isEnabledSend = (checkLoginFormatUseCase.invoke(intent.textLogin) && checkPasswordFormatUseCase.invoke(
intent.textLogin, intent.textPassword
)),
error = null error = null
) )
} }

View File

@ -2,7 +2,8 @@
<string name="app_name">Work</string> <string name="app_name">Work</string>
<string name="title_activity_root">Приложение</string> <string name="title_activity_root">Приложение</string>
<string name="auth_title">Привет! Введи код для авторизации</string> <string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</string> <string name="auth_label_login">Логин</string>
<string name="auth_label_password">Пароль</string>
<string name="auth_sign_in">Войти</string> <string name="auth_sign_in">Войти</string>
<string name="main_refresh">Обновить</string> <string name="main_refresh">Обновить</string>