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()
context = this
}
companion object {
lateinit var context: Context
}

View File

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

View File

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

View File

@ -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<Boolean> {
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<String> {
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)
}
}

View File

@ -10,7 +10,7 @@ class BookRepository(
private val authRepository: AuthRepository
) {
suspend fun getInfo(): Result<MainInfoEntity> {
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<List<BookingData>> {
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<Boolean> {
val code = authRepository.getCode() ?: return getNoAuthResult()
val code = authRepository.getToken() ?: return getNoAuthResult()
val dto = BookRequestDto(data.date, data.placeId)
return NetworkDataSource.addBook(code, dto)
}

View File

@ -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<Boolean> = withContext(Dispatchers.IO) {
suspend fun getToken(login: String, password: String): Result<String> = 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<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) {
HttpStatusCode.OK -> true
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 {
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<UserDto>()
} else {
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 {
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<Map<String, List<PlaceDto>>>()
} 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 {
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"
}

View File

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

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
class GetCodeUseCase(
class GetTokenLocalUseCase(
private val repository: AuthRepository
) {
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.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<AppDestination?>(null) }
LaunchedEffect(Unit) {
val code = GetCodeUseCase(AuthRepository).invoke()
destination = if (code == null) {
val token = GetTokenLocalUseCase(AuthRepository).invoke()
destination = if (token == null) {
AuthScreenDestination
} else {
MainScreenDestination

View File

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

View File

@ -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
) {

View File

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

View File

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