Compare commits

..

9 Commits

Author SHA1 Message Date
c4913ec0bc merge upstream 2025-11-29 15:13:20 +00:00
2ecbc7339a AuthRepository.kt fix
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled
2025-11-29 18:08:05 +03:00
00162984a9 merge upstream 2025-11-29 15:03:42 +00:00
bc7e14ab06 AuthRepository.kt fix
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled
2025-11-29 16:27:55 +03:00
b98c7f410f add auth interface logical 2025-11-29 13:03:50 +00:00
67e26d142a merge upstream 2025-11-29 12:52:36 +00:00
«Владимир
286b752974 add auth interface logical 2025-11-29 15:51:41 +03:00
cbc91ee472 Auth Case and Repo done 2025-11-29 15:37:18 +03:00
«Владимир
d92ac5a10c add space 2025-11-29 14:40:25 +03:00
11 changed files with 158 additions and 18 deletions

View File

@@ -8,9 +8,7 @@ object AuthRepository {
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
codeCache = text
}
} }
} }
} }

View File

@@ -0,0 +1,54 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.source.NetworkDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import ru.myitschool.work.domain.auth.entities.BookingEntity
import ru.myitschool.work.domain.auth.entities.UserEntity
import kotlinx.serialization.json.Json
object MainRepository {
@Serializable
data class BookingDto(
val roomName: String,
val time: String
)
@Serializable
data class UserDto(
val name: String,
val bookingList: List<BookingDto>
)
private val json = Json { ignoreUnknownKeys = true }
private var codeCache: String? = null
private var cachedUser: UserEntity? = null
suspend fun getUserInfo(code: String): Result<UserEntity> {
return withContext(Dispatchers.IO) {
runCatching {
if (codeCache == code && cachedUser != null) return@runCatching cachedUser!!
codeCache = code
val isAuth = NetworkDataSource.checkAuth(code).getOrThrow()
if (!isAuth) throw Exception("Авторизация не пройдена")
val userJson = NetworkDataSource.getUserData(code).getOrThrow()
val userResponse = json.decodeFromString<UserDto>(userJson)
val user = UserEntity(
name = userResponse.name,
bookingList = userResponse.bookingList.map { BookingEntity(it.roomName, it.time) }
)
cachedUser = user
user
}
}
}
}

View File

@@ -29,7 +29,7 @@ object NetworkDataSource {
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
@@ -38,5 +38,15 @@ object NetworkDataSource {
} }
} }
suspend fun getUserData(code: String): Result<String> = withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> response.bodyAsText()
else -> error(response.bodyAsText())
}
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
} }

View File

@@ -8,6 +8,10 @@ class CheckAndSaveAuthCodeUseCase(
suspend operator fun invoke( suspend operator fun invoke(
text: String text: String
): Result<Unit> { ): Result<Unit> {
val isValid = text.length == 4 && text.all {it.isLetterOrDigit()}
if (!isValid) return Result.failure(IllegalArgumentException("Неверный формат кода!"))
return repository.checkAndSave(text).mapCatching { success -> return repository.checkAndSave(text).mapCatching { success ->
if (!success) error("Code is incorrect") if (!success) error("Code is incorrect")
} }

View File

@@ -0,0 +1,12 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.auth.entities.UserEntity
class GetUserInfoUseCase(
private val repository: MainRepository
) {
suspend operator fun invoke(code: String): Result<UserEntity> {
return repository.getUserInfo(code)
}
}

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.domain.auth.entities
data class BookingEntity(
val roomName: String,
val time: String,
)

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.domain.auth.entities
data class UserEntity(
val name: String,
val bookingList: List<BookingEntity>
)

View File

@@ -21,7 +21,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -32,6 +31,7 @@ import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable @Composable
fun AuthScreen( fun AuthScreen(
viewModel: AuthViewModel = viewModel(), viewModel: AuthViewModel = viewModel(),
@@ -74,7 +74,12 @@ private fun Content(
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
val isCodeValid = inputText.length == 4 && inputText.all { it.isLetterOrDigit() }
val isButtonEnabled = isCodeValid
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.CODE_INPUT).fillMaxWidth(),
value = inputText, value = inputText,
@@ -82,16 +87,30 @@ private fun Content(
inputText = it inputText = it
viewModel.onIntent(AuthIntent.TextInput(it)) viewModel.onIntent(AuthIntent.TextInput(it))
}, },
label = { Text(stringResource(R.string.auth_label)) } label = { Text(stringResource(R.string.auth_label)) },
placeholder = { Text("Код") },
isError = state.error != null
) )
if (state.error != null) {
Spacer(modifier = Modifier.size(8.dp))
Text(
text = state.error,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.testTag(TestIds.Auth.ERROR)
)
}
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(inputText))
}, },
enabled = true enabled = isButtonEnabled
) { ) {
Text(stringResource(R.string.auth_sign_in)) Text(stringResource(R.string.auth_sign_in))
} }
} }

View File

@@ -1,6 +1,8 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
sealed interface AuthState { sealed class AuthState {
object Loading: AuthState object Loading : AuthState()
object Data: AuthState data class Data(
val error: String? = null
) : AuthState()
} }

View File

@@ -15,7 +15,7 @@ import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class AuthViewModel : ViewModel() { class AuthViewModel : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data) private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
val uiState: StateFlow<AuthState> = _uiState.asStateFlow() val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
@@ -24,20 +24,30 @@ class AuthViewModel : ViewModel() {
fun onIntent(intent: AuthIntent) { fun onIntent(intent: AuthIntent) {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
val code = intent.text
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
_uiState.update { AuthState.Loading } _uiState.value = AuthState.Loading
checkAndSaveAuthCodeUseCase.invoke("9999").fold( checkAndSaveAuthCodeUseCase.invoke(code).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(Unit) _actionFlow.emit(Unit)
}, },
onFailure = { error -> onFailure = { error ->
error.printStackTrace()
_actionFlow.emit(Unit) val errorMsg = when {
error.message?.contains("401") == true -> "Неверный код"
else -> "Ошибка подключения"
}
_uiState.value = AuthState.Data(error = errorMsg)
} }
) )
} }
} }
is AuthIntent.TextInput -> Unit
is AuthIntent.TextInput -> {
if (_uiState.value is AuthState.Data) {
_uiState.value = AuthState.Data(error = null)
}
}
} }
} }
} }

View File

@@ -0,0 +1,19 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainState {
object Loading : MainState
data class Success(
val name: String,
val photoUrl: String,
val bookings: List<BookingItem>,
) : MainState
data class Error(
val message: String,
) : MainState
}
data class BookingItem(
val date: String,
val place: String,
)