checkpoint 1

This commit is contained in:
Egor 2026-02-25 17:15:34 +03:00
parent d7b5212af6
commit 614640615f
17 changed files with 139 additions and 69 deletions

View File

@ -8,7 +8,6 @@ class App: Application() {
super.onCreate() super.onCreate()
context = this context = this
} }
companion object { companion object {
lateinit var context: Context lateinit var context: Context
} }

View File

@ -1,9 +1,10 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { 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 AUTH_URL = "/auth"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" const val BOOKING_URL = "/booking"
const val BOOK_URL = "/book" const val BOOK_URL = "/book"
const val GET_TOKEN = "/getToken"
} }

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

@ -12,37 +12,38 @@ import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository { object AuthRepository {
private const val STORE = "AUTH-STORE" private const val STORE = "AUTH-STORE"
private const val CODE_KEY = "CODE" private const val TOKEN_KEY = "TOKEN"
private var tokenCache: String? = null
private var codeCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> { suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success -> return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) { if (success) {
codeCache = text tokenCache = text
App.context.userDataStore.edit { preferences -> App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(CODE_KEY) val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences[prefKey] = text preferences[prefKey] = text
} }
} }
} }
} }
suspend fun getToken(): String? {
suspend fun getCode(): String? { if (tokenCache == null) {
if (codeCache == null) { tokenCache = App.context.userDataStore.data
codeCache = App.context.userDataStore.data
.firstOrNull() .firstOrNull()
?.let { preferences -> ?.let { preferences ->
preferences[stringPreferencesKey(CODE_KEY)] preferences[stringPreferencesKey(TOKEN_KEY)]
} }
} }
return codeCache return tokenCache
}
suspend fun getToken(login : String, password : String) : Result<String> {
return NetworkDataSource.getToken(login, password)
} }
suspend fun logout() { suspend fun logout() {
codeCache = null tokenCache = null
App.context.userDataStore.edit { preferences -> App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(CODE_KEY) val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences.remove(prefKey) preferences.remove(prefKey)
} }
} }

View File

@ -10,7 +10,7 @@ class BookRepository(
private val authRepository: AuthRepository private val authRepository: AuthRepository
) { ) {
suspend fun getInfo(): Result<MainInfoEntity> { suspend fun getInfo(): Result<MainInfoEntity> {
val code = authRepository.getCode() ?: return getNoAuthResult() val code = authRepository.getToken() ?: return getNoAuthResult()
return NetworkDataSource.getInfo(code).mapCatching { dto -> return NetworkDataSource.getInfo(code).mapCatching { dto ->
MainInfoEntity( MainInfoEntity(
name = dto.name ?: error("Name is null"), name = dto.name ?: error("Name is null"),
@ -26,7 +26,7 @@ class BookRepository(
} }
suspend fun getBookingInfo(): Result<List<BookingData>> { suspend fun getBookingInfo(): Result<List<BookingData>> {
val code = authRepository.getCode() ?: return getNoAuthResult() val code = authRepository.getToken() ?: return getNoAuthResult()
return NetworkDataSource.getBooking(code).mapCatching { dto -> return NetworkDataSource.getBooking(code).mapCatching { dto ->
dto?.map { (date, places) -> dto?.map { (date, places) ->
BookingData( BookingData(
@ -43,7 +43,7 @@ class BookRepository(
} }
suspend fun sendBook(data: BookRequestData): Result<Boolean> { suspend fun sendBook(data: BookRequestData): Result<Boolean> {
val code = authRepository.getCode() ?: return getNoAuthResult() val code = authRepository.getToken() ?: return getNoAuthResult()
val dto = BookRequestDto(data.date, data.placeId) val dto = BookRequestDto(data.date, data.placeId)
return NetworkDataSource.addBook(code, dto) return NetworkDataSource.addBook(code, dto)
} }

View File

@ -1,13 +1,17 @@
package ru.myitschool.work.data.source package ru.myitschool.work.data.source
import android.accounts.NetworkErrorException
import android.util.Log
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.contentType
@ -15,6 +19,7 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.PlaceDto import ru.myitschool.work.data.dto.PlaceDto
import ru.myitschool.work.data.dto.BookRequestDto import ru.myitschool.work.data.dto.BookRequestDto
@ -35,10 +40,29 @@ object NetworkDataSource {
} }
} }
} }
suspend fun getToken(login: String, password: String): Result<String> = withContext(Dispatchers.IO) {
suspend fun checkAuth(code: 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("${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<String>()
}
}
suspend fun checkAuth(token: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(token, Constants.AUTH_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> false else -> false
@ -46,12 +70,13 @@ object NetworkDataSource {
} }
} }
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) { suspend fun getInfo(token: String): Result<UserDto> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
println("!!!!!!!!!!!!!! getInfo $code") println("!!!!!!!!!!!!!! getInfo $token")
val response = client.get(getUrl(code, Constants.INFO_URL)) val response = client.get(getUrl(token, Constants.INFO_URL))
if (response.status == HttpStatusCode.OK) { if (response.status == HttpStatusCode.OK) {
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
Log.d("1", "!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
response.body<UserDto>() response.body<UserDto>()
} else { } else {
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}") println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
@ -60,9 +85,9 @@ object NetworkDataSource {
} }
} }
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) { suspend fun getBooking(token: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
return@withContext runCatching { 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) { if (response.status == HttpStatusCode.OK) {
response.body<Map<String, List<PlaceDto>>>() response.body<Map<String, List<PlaceDto>>>()
} else { } else {
@ -71,9 +96,9 @@ object NetworkDataSource {
} }
} }
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun addBook(token: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { 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) contentType(ContentType.Application.Json)
setBody(data) 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"
} }

View File

@ -2,7 +2,7 @@ package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase( class CheckAndSaveAuthUseCase(
private val repository: AuthRepository private val repository: AuthRepository
) { ) {
suspend operator fun invoke( suspend operator fun invoke(

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,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())
}
}
}

View File

@ -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
}
}
}

View File

@ -2,10 +2,10 @@ package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
class GetCodeUseCase( class GetTokenLocalUseCase(
private val repository: AuthRepository private val repository: AuthRepository
) { ) {
suspend operator fun invoke(): String? { suspend operator fun invoke(): String? {
return repository.getCode() return repository.getToken()
} }
} }

View File

@ -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<String> {
return repository.getToken(login, password)
}
}

View File

@ -13,9 +13,9 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
import ru.myitschool.work.data.repo.AuthRepository 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.AppDestination
import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination
@ -31,8 +31,8 @@ fun AppNavHost(
) { ) {
var destination by remember { mutableStateOf<AppDestination?>(null) } var destination by remember { mutableStateOf<AppDestination?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val code = GetCodeUseCase(AuthRepository).invoke() val token = GetTokenLocalUseCase(AuthRepository).invoke()
destination = if (code == null) { destination = if (token == null) {
AuthScreenDestination AuthScreenDestination
} else { } else {
MainScreenDestination MainScreenDestination

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 login: String, val password: String): AuthIntent
data class TextInput(val text: String): AuthIntent data class TextInput(val login: String, val password: String): AuthIntent
} }

View File

@ -47,7 +47,6 @@ fun AuthScreen(
} }
} }
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -70,28 +69,42 @@ fun AuthScreen(
} }
} }
} }
@Composable
fun SecureScreen(enabled: Boolean = true) {
}
@Composable @Composable
private fun Content( private fun Content(
viewModel: AuthViewModel, viewModel: AuthViewModel,
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") } var inputTextLogin by remember { mutableStateOf("") }
var inputTextPassword by remember { 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 = inputTextLogin,
onValueChange = { onValueChange = {
inputText = it inputTextLogin = it
viewModel.onIntent(AuthIntent.TextInput(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)) 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(inputTextLogin, inputTextPassword))
}, },
enabled = state.isEnabledSend enabled = state.isEnabledSend
) { ) {

View File

@ -10,13 +10,15 @@ 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.CheckLoginFormatUseCase
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
import ru.myitschool.work.domain.auth.GetTokenNetworkUseCase
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 checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
private val getTokenNetworkUseCase by lazy { GetTokenNetworkUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>( private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data( AuthState.Data(
isEnabledSend = false, isEnabledSend = false,
@ -32,14 +34,14 @@ class AuthViewModel : ViewModel() {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
viewModelScope.launch { viewModelScope.launch {
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( getTokenNetworkUseCase.invoke(intent.login, intent.password).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(AuthAction.Open(MainScreenDestination)) _actionFlow.emit(AuthAction.Open(MainScreenDestination))
}, },
onFailure = { error -> onFailure = { error ->
updateStateIfData { oldState -> updateStateIfData { oldState ->
oldState.copy( oldState.copy(
error = error.message error = "Неизвестная ошибка: ${error.message}"
) )
} }
} }
@ -49,7 +51,7 @@ 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.login) && checkPasswordFormatUseCase.invoke(intent.password),
error = null error = null
) )
} }

View File

@ -1,8 +1,9 @@
<resources> <resources>
<string name="app_name">Work</string> <string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string> <string name="title_activity_root">RootActivity</string>
<string name="auth_title">Привет! Введи код для авторизации</string> <string name="auth_title">Привет! Введи свой логин и пароль для авторизации</string>
<string name="auth_label">Код</string> <string name="auth_login">Логин</string>
<string name="auth_password">Пароль</string>
<string name="auth_sign_in">Войти</string> <string name="auth_sign_in">Войти</string>
<string name="main_refresh">Обновить</string> <string name="main_refresh">Обновить</string>