From 614640615f9e3d2195e03425103463981ae85203 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 25 Feb 2026 17:15:34 +0300 Subject: [PATCH] checkpoint 1 --- app/src/main/java/ru/myitschool/work/App.kt | 1 - .../java/ru/myitschool/work/core/Constants.kt | 3 +- .../java/ru/myitschool/work/core/TestIds.kt | 3 +- .../work/data/repo/AuthRepository.kt | 29 ++++++------ .../work/data/repo/BookRepository.kt | 6 +-- .../work/data/source/NetworkDataSource.kt | 47 ++++++++++++++----- ...eUseCase.kt => CheckAndSaveAuthUseCase.kt} | 2 +- .../domain/auth/CheckCodeFormatUseCase.kt | 12 ----- .../domain/auth/CheckLoginFormatUseCase.kt | 9 ++++ .../domain/auth/CheckPasswordFormatUseCase.kt | 19 ++++++++ ...CodeUseCase.kt => GetTokenLocalUseCase.kt} | 4 +- .../domain/auth/GetTokenNetworkUseCase.kt | 11 +++++ .../work/ui/screen/NavigationGraph.kt | 8 ++-- .../work/ui/screen/auth/AuthIntent.kt | 4 +- .../work/ui/screen/auth/AuthScreen.kt | 29 ++++++++---- .../work/ui/screen/auth/AuthViewModel.kt | 16 ++++--- app/src/main/res/values/strings.xml | 5 +- 17 files changed, 139 insertions(+), 69 deletions(-) rename app/src/main/java/ru/myitschool/work/domain/auth/{CheckAndSaveAuthCodeUseCase.kt => CheckAndSaveAuthUseCase.kt} (91%) delete mode 100644 app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/auth/CheckLoginFormatUseCase.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/auth/CheckPasswordFormatUseCase.kt rename app/src/main/java/ru/myitschool/work/domain/auth/{GetCodeUseCase.kt => GetTokenLocalUseCase.kt} (74%) create mode 100644 app/src/main/java/ru/myitschool/work/domain/auth/GetTokenNetworkUseCase.kt diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt index aa33483..21d93f4 100644 --- a/app/src/main/java/ru/myitschool/work/App.kt +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -8,7 +8,6 @@ class App: Application() { super.onCreate() context = this } - companion object { lateinit var context: Context } diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index 00f706d..f229fb7 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,9 +1,10 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://10.0.0.14:49182" // "http://10.0.0.14:49182" + const val HOST = "http://10.0.0.14:49182" // "http://10.0.0.14:49182" or "http://10.0.2.2:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" const val BOOK_URL = "/book" + const val GET_TOKEN = "/getToken" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/TestIds.kt b/app/src/main/java/ru/myitschool/work/core/TestIds.kt index d67b884..048ea60 100644 --- a/app/src/main/java/ru/myitschool/work/core/TestIds.kt +++ b/app/src/main/java/ru/myitschool/work/core/TestIds.kt @@ -4,7 +4,8 @@ object TestIds { object Auth { const val ERROR = "auth_error" 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 { const val ERROR = "main_error" diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index e4126dd..0f29fc1 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -12,37 +12,38 @@ import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { private const val STORE = "AUTH-STORE" - private const val CODE_KEY = "CODE" - - private var codeCache: String? = null - + private const val TOKEN_KEY = "TOKEN" + private var tokenCache: String? = null suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> if (success) { - codeCache = text + tokenCache = text App.context.userDataStore.edit { preferences -> - val prefKey = stringPreferencesKey(CODE_KEY) + val prefKey = stringPreferencesKey(TOKEN_KEY) preferences[prefKey] = text } } } } - - suspend fun getCode(): String? { - if (codeCache == null) { - codeCache = App.context.userDataStore.data + suspend fun getToken(): String? { + if (tokenCache == null) { + tokenCache = App.context.userDataStore.data .firstOrNull() ?.let { preferences -> - preferences[stringPreferencesKey(CODE_KEY)] + preferences[stringPreferencesKey(TOKEN_KEY)] } } - return codeCache + return tokenCache + } + + suspend fun getToken(login : String, password : String) : Result { + return NetworkDataSource.getToken(login, password) } suspend fun logout() { - codeCache = null + tokenCache = null App.context.userDataStore.edit { preferences -> - val prefKey = stringPreferencesKey(CODE_KEY) + val prefKey = stringPreferencesKey(TOKEN_KEY) preferences.remove(prefKey) } } diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt index ea1c581..94c484d 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -10,7 +10,7 @@ class BookRepository( private val authRepository: AuthRepository ) { suspend fun getInfo(): Result { - val code = authRepository.getCode() ?: return getNoAuthResult() + val code = authRepository.getToken() ?: return getNoAuthResult() return NetworkDataSource.getInfo(code).mapCatching { dto -> MainInfoEntity( name = dto.name ?: error("Name is null"), @@ -26,7 +26,7 @@ class BookRepository( } suspend fun getBookingInfo(): Result> { - val code = authRepository.getCode() ?: return getNoAuthResult() + val code = authRepository.getToken() ?: return getNoAuthResult() return NetworkDataSource.getBooking(code).mapCatching { dto -> dto?.map { (date, places) -> BookingData( @@ -43,7 +43,7 @@ class BookRepository( } suspend fun sendBook(data: BookRequestData): Result { - val code = authRepository.getCode() ?: return getNoAuthResult() + val code = authRepository.getToken() ?: return getNoAuthResult() val dto = BookRequestDto(data.date, data.placeId) return NetworkDataSource.addBook(code, dto) } diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 85387ac..6921cc0 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -1,13 +1,17 @@ package ru.myitschool.work.data.source +import android.accounts.NetworkErrorException +import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.HttpRequestData import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.contentType @@ -15,6 +19,7 @@ import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray import ru.myitschool.work.core.Constants import ru.myitschool.work.data.dto.PlaceDto import ru.myitschool.work.data.dto.BookRequestDto @@ -35,10 +40,29 @@ object NetworkDataSource { } } } - - suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { + suspend fun getToken(login: String, password: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) + val response = client.post("${Constants.HOST}/api${Constants.GET_TOKEN}") { + contentType(ContentType.Application.Json) + setBody( + """ + "login" : "$login", + "password" : "$password" + """.trimIndent() + ) + } + if (response.status != HttpStatusCode.OK) { + error(response.status) + } + else if (response.status == HttpStatusCode.Unauthorized) { + error("Неверный логин или пароль") + } + response.body() + } + } + suspend fun checkAuth(token: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(token, Constants.AUTH_URL)) when (response.status) { HttpStatusCode.OK -> true else -> false @@ -46,12 +70,13 @@ object NetworkDataSource { } } - suspend fun getInfo(code: String): Result = withContext(Dispatchers.IO) { + suspend fun getInfo(token: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - println("!!!!!!!!!!!!!! getInfo $code") - val response = client.get(getUrl(code, Constants.INFO_URL)) + println("!!!!!!!!!!!!!! getInfo $token") + val response = client.get(getUrl(token, Constants.INFO_URL)) if (response.status == HttpStatusCode.OK) { println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") + Log.d("1", "!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") response.body() } else { println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}") @@ -60,9 +85,9 @@ object NetworkDataSource { } } - suspend fun getBooking(code: String): Result>?> = withContext(Dispatchers.IO) { + suspend fun getBooking(token: String): Result>?> = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.BOOKING_URL)) + val response = client.get(getUrl(token, Constants.BOOKING_URL)) if (response.status == HttpStatusCode.OK) { response.body>>() } else { @@ -71,9 +96,9 @@ object NetworkDataSource { } } - suspend fun addBook(code: String, data: BookRequestDto): Result = withContext(Dispatchers.IO) { + suspend fun addBook(token: String, data: BookRequestDto): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.post(getUrl(code, Constants.BOOK_URL)) { + val response = client.post(getUrl(token, Constants.BOOK_URL)) { contentType(ContentType.Application.Json) setBody(data) } @@ -85,5 +110,5 @@ object NetworkDataSource { } } - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" + private fun getUrl(token: String, targetUrl: String) = "${Constants.HOST}/api/$token$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthUseCase.kt similarity index 91% rename from app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt rename to app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthUseCase.kt index 012fb6f..460d80e 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthUseCase.kt @@ -2,7 +2,7 @@ package ru.myitschool.work.domain.auth import ru.myitschool.work.data.repo.AuthRepository -class CheckAndSaveAuthCodeUseCase( +class CheckAndSaveAuthUseCase( private val repository: AuthRepository ) { suspend operator fun invoke( diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt deleted file mode 100644 index fe291a0..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt +++ /dev/null @@ -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()) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckLoginFormatUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckLoginFormatUseCase.kt new file mode 100644 index 0000000..f1ebfc3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckLoginFormatUseCase.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.auth + +class CheckLoginFormatUseCase { + operator fun invoke(login: String): Boolean { + return login.all { char -> char.isLetterOrDigit() && + ((char in 'A'..'Z') || (char in 'a'..'z') || char.isDigit()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckPasswordFormatUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckPasswordFormatUseCase.kt new file mode 100644 index 0000000..2fc879d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckPasswordFormatUseCase.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work.domain.auth + +import kotlin.text.all + +class CheckPasswordFormatUseCase { + operator fun invoke( + password: String + ): Boolean { + return password.length >= 8 && password.all{char -> + (char in 'A'..'Z' || char in 'a'..'z' || char.isDigit()) || (char == '!' || char == '@' || + char == '#' || char == '$' || char == '&' || char == '*')} + } + fun validatePassword(password: String, login: String): Boolean { + val loginChars = login.lowercase().toSet() + return !password.lowercase().any{ + it in loginChars + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenLocalUseCase.kt similarity index 74% rename from app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt rename to app/src/main/java/ru/myitschool/work/domain/auth/GetTokenLocalUseCase.kt index a3c22b8..ff3219d 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenLocalUseCase.kt @@ -2,10 +2,10 @@ package ru.myitschool.work.domain.auth import ru.myitschool.work.data.repo.AuthRepository -class GetCodeUseCase( +class GetTokenLocalUseCase( private val repository: AuthRepository ) { suspend operator fun invoke(): String? { - return repository.getCode() + return repository.getToken() } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenNetworkUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenNetworkUseCase.kt new file mode 100644 index 0000000..d1edf62 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenNetworkUseCase.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.domain.auth + +import ru.myitschool.work.data.repo.AuthRepository + +class GetTokenNetworkUseCase( + private val repository: AuthRepository +) { + suspend operator fun invoke(login : String, password: String): Result { + return repository.getToken(login, password) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 3590d24..bcd283f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -13,9 +13,9 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.delay import ru.myitschool.work.data.repo.AuthRepository -import ru.myitschool.work.domain.auth.GetCodeUseCase +import ru.myitschool.work.domain.auth.GetTokenLocalUseCase +import ru.myitschool.work.domain.auth.GetTokenNetworkUseCase import ru.myitschool.work.ui.nav.AppDestination import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination @@ -31,8 +31,8 @@ fun AppNavHost( ) { var destination by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - val code = GetCodeUseCase(AuthRepository).invoke() - destination = if (code == null) { + val token = GetTokenLocalUseCase(AuthRepository).invoke() + destination = if (token == null) { AuthScreenDestination } else { MainScreenDestination diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt index 74f200a..b4aec57 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt @@ -1,6 +1,6 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthIntent { - data class Send(val text: String): AuthIntent - data class TextInput(val text: String): AuthIntent + data class Send(val login: String, val password: String): AuthIntent + data class TextInput(val login: String, val password: String): AuthIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 4b91b98..7bee4f7 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -47,7 +47,6 @@ fun AuthScreen( } } } - Column( modifier = Modifier .fillMaxSize() @@ -70,28 +69,42 @@ fun AuthScreen( } } } +@Composable +fun SecureScreen(enabled: Boolean = true) { +} @Composable private fun Content( viewModel: AuthViewModel, state: AuthState.Data ) { - var inputText by remember { mutableStateOf("") } + var inputTextLogin by remember { mutableStateOf("") } + var inputTextPassword by remember { mutableStateOf("") } Spacer(modifier = Modifier.size(16.dp)) TextField( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), - value = inputText, + modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(), + value = inputTextLogin, onValueChange = { - inputText = it - viewModel.onIntent(AuthIntent.TextInput(it)) + inputTextLogin = it + viewModel.onIntent(AuthIntent.TextInput(inputTextLogin, inputTextPassword)) }, - label = { Text(stringResource(R.string.auth_label)) } + label = { Text(stringResource(R.string.auth_login)) } + ) + Spacer(modifier = Modifier.size(16.dp)) + TextField( + modifier = Modifier.testTag(TestIds.Auth.PASSWORD_INPUT).fillMaxWidth(), + value = inputTextPassword, + onValueChange = { + inputTextPassword = it + viewModel.onIntent(AuthIntent.TextInput(inputTextLogin, inputTextPassword)) + }, + label = { Text(stringResource(R.string.auth_password)) } ) Spacer(modifier = Modifier.size(16.dp)) Button( modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) + viewModel.onIntent(AuthIntent.Send(inputTextLogin, inputTextPassword)) }, enabled = state.isEnabledSend ) { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index c28f5cd..b85b40d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -10,13 +10,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository -import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase -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.domain.auth.GetTokenNetworkUseCase import ru.myitschool.work.ui.nav.MainScreenDestination class AuthViewModel : ViewModel() { - private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() } - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } + private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() } + private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() } + private val getTokenNetworkUseCase by lazy { GetTokenNetworkUseCase(AuthRepository) } private val _uiState = MutableStateFlow( AuthState.Data( isEnabledSend = false, @@ -32,14 +34,14 @@ class AuthViewModel : ViewModel() { when (intent) { is AuthIntent.Send -> { viewModelScope.launch { - checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( + getTokenNetworkUseCase.invoke(intent.login, intent.password).fold( onSuccess = { _actionFlow.emit(AuthAction.Open(MainScreenDestination)) }, onFailure = { error -> updateStateIfData { oldState -> oldState.copy( - error = error.message + error = "Неизвестная ошибка: ${error.message}" ) } } @@ -49,7 +51,7 @@ class AuthViewModel : ViewModel() { is AuthIntent.TextInput -> { updateStateIfData { oldState -> oldState.copy( - isEnabledSend = checkCodeFormatUseCase.invoke(intent.text), + isEnabledSend = checkLoginFormatUseCase.invoke(intent.login) && checkPasswordFormatUseCase.invoke(intent.password), error = null ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9273cf..0e0eb8f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ Work RootActivity - Привет! Введи код для авторизации - Код + Привет! Введи свой логин и пароль для авторизации + Логин + Пароль Войти Обновить