main #6

Closed
student-d-sherstnev wants to merge 19 commits from Minipigi-org/NTO-2026-Android-TeamTask-Template:main into main
9 changed files with 143 additions and 56 deletions
Showing only changes of commit 23f8fa6db0 - Show all commits

View File

@ -0,0 +1,20 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AuthRequestDto(
@SerialName("login")
val login: String,
@SerialName("password")
val password: String,
)
@Serializable
data class AuthResponseDto(
@SerialName("token")
val token: String,
@SerialName("expired")
val expired: Int,
)

View File

@ -8,41 +8,42 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import ru.myitschool.work.App import ru.myitschool.work.App
import ru.myitschool.work.data.dto.AuthRequestDto
import ru.myitschool.work.data.dto.AuthResponseDto
import ru.myitschool.work.data.source.NetworkDataSource 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 codeCache: String? = null private var tokenCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> { suspend fun checkAndSave(login: String, password: String): Result<AuthResponseDto> {
return NetworkDataSource.checkAuth(text).onSuccess { success -> val data = AuthRequestDto(login=login, password=password)
if (success) { return NetworkDataSource.checkAuth(data).onSuccess { success ->
codeCache = text tokenCache = success.token
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] = success.token
}
} }
} }
} }
suspend fun getCode(): String? { suspend fun getToken(): String? {
if (codeCache == null) { if (tokenCache == null) {
codeCache = App.context.userDataStore.data tokenCache = App.context.userDataStore.data
.firstOrNull() .firstOrNull()
?.let { preferences -> ?.let { preferences ->
preferences[stringPreferencesKey(CODE_KEY)] preferences[stringPreferencesKey(TOKEN_KEY)]
} }
} }
return codeCache return tokenCache
} }
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,8 +10,8 @@ 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 token = authRepository.getToken() ?: return getNoAuthResult()
return NetworkDataSource.getInfo(code).mapCatching { dto -> return NetworkDataSource.getInfo(token).mapCatching { dto ->
MainInfoEntity( MainInfoEntity(
name = dto.name ?: error("Name is null"), name = dto.name ?: error("Name is null"),
photoUrl = dto.photoUrl ?: error("Photo url is null"), photoUrl = dto.photoUrl ?: error("Photo url is null"),
@ -26,8 +26,8 @@ class BookRepository(
} }
suspend fun getBookingInfo(): Result<List<BookingData>> { suspend fun getBookingInfo(): Result<List<BookingData>> {
val code = authRepository.getCode() ?: return getNoAuthResult() val token = authRepository.getToken() ?: return getNoAuthResult()
return NetworkDataSource.getBooking(code).mapCatching { dto -> return NetworkDataSource.getBooking(token).mapCatching { dto ->
dto?.map { (date, places) -> dto?.map { (date, places) ->
BookingData( BookingData(
date = date, date = date,
@ -43,9 +43,9 @@ class BookRepository(
} }
suspend fun sendBook(data: BookRequestData): Result<Boolean> { suspend fun sendBook(data: BookRequestData): Result<Boolean> {
val code = authRepository.getCode() ?: return getNoAuthResult() val token = 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(token, dto)
} }
private fun <T> getNoAuthResult() = Result.failure<T>( private fun <T> getNoAuthResult() = Result.failure<T>(
IllegalStateException("No auth") IllegalStateException("No auth")

View File

@ -5,6 +5,7 @@ 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.get import io.ktor.client.request.get
import io.ktor.client.request.headers
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
@ -16,6 +17,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.AuthRequestDto
import ru.myitschool.work.data.dto.AuthResponseDto
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
import ru.myitschool.work.data.dto.UserDto import ru.myitschool.work.data.dto.UserDto
@ -36,20 +39,27 @@ object NetworkDataSource {
} }
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(data: AuthRequestDto): Result<AuthResponseDto> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.post(getUrl(Constants.BOOK_URL)) {
contentType(ContentType.Application.Json)
setBody(data)
}
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> response.body<AuthResponseDto>()
else -> false else -> error(response.bodyAsText())
} }
} }
} }
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")
val response = client.get(getUrl(code, Constants.INFO_URL)) val response = client.get(getUrl(Constants.INFO_URL)) {
headers {
append("Authorization", "Bearer $token")
}
}
if (response.status == HttpStatusCode.OK) { if (response.status == HttpStatusCode.OK) {
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
response.body<UserDto>() response.body<UserDto>()
@ -60,9 +70,13 @@ 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(Constants.BOOKING_URL)) {
headers {
append("Authorization", "Bearer $token")
}
}
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,11 +85,14 @@ 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(Constants.BOOK_URL)) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody(data) setBody(data)
headers {
append("Authorization", "Bearer $token")
}
} }
when (response.status) { when (response.status) {
HttpStatusCode.Created -> true HttpStatusCode.Created -> true
@ -85,5 +102,5 @@ object NetworkDataSource {
} }
} }
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" private fun getUrl(targetUrl: String) = "${Constants.HOST}/api/$targetUrl"
} }

View File

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

View File

@ -2,11 +2,18 @@ package ru.myitschool.work.domain.auth
class CheckCodeFormatUseCase { class CheckCodeFormatUseCase {
operator fun invoke( operator fun invoke(
text: String login: String, password: String
): Boolean { ): Boolean {
return text.length == 4 && text.all { char -> val passwordList = password.toList()
char.isLetterOrDigit() && var passwordCorrect = true
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit()) for (i in 1..(passwordList.size - 3)) {
val s = StringBuilder().append(passwordList[i]).append(passwordList[i+1]).append(passwordList[i+2]).toString()
if (login.contains(s)) {
passwordCorrect = false
} }
} }
return login.isNotEmpty() && password.length >= 8 && passwordCorrect && login.all { char ->
char.isLetterOrDigit() && ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
} && password.all({ char -> password.count { it == char } < 3 }) && password.count { it == '!' } + password.count { it == '@' } + password.count { it == '#' } + password.count { it == '"' } + password.count { it == '№' } + password.count { it == ';' } + password.count { it == '$' } + password.count { it == '%' } + password.count { it == '^' } + password.count { it == ':' } + password.count { it == '&' } + password.count { it == '?' } + password.count { it == '*' } >= 1
}
} }

View File

@ -6,6 +6,6 @@ class GetCodeUseCase(
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

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

@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
import android.os.CountDownTimer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -28,15 +29,51 @@ class AuthViewModel : ViewModel() {
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
val actionFlow: SharedFlow<AuthAction> = _actionFlow val actionFlow: SharedFlow<AuthAction> = _actionFlow
private var authTries: Int = 0
private var timerSecs: Int = 0
fun onIntent(intent: AuthIntent) { fun onIntent(intent: AuthIntent) {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
if (authTries >= 5) {
timerSecs = 60
val timer = object : CountDownTimer((timerSecs * 1000).toLong(), 1000) {
override fun onTick(millisUntilFinished: Long) {
timerSecs -= 1
updateStateIfData { oldState ->
oldState.copy(
error = "Слишком много попыток входа, попробуйте через $timerSecs секунд"
)
}
}
override fun onFinish() {
authTries = 0
updateStateIfData { oldState ->
oldState.copy(
isEnabledSend = checkCodeFormatUseCase.invoke(
login = intent.login,
password = intent.password
),
error = null
)
}
}
}
timer.start()
updateStateIfData { oldState ->
oldState.copy(
error = "Слишком много попыток входа, попробуйте через $timerSecs секунд"
)
}
} else {
viewModelScope.launch { viewModelScope.launch {
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( checkAndSaveAuthCodeUseCase.invoke(intent.login, intent.password).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(AuthAction.Open(MainScreenDestination)) _actionFlow.emit(AuthAction.Open(MainScreenDestination))
}, },
onFailure = { error -> onFailure = { error ->
authTries += 1
updateStateIfData { oldState -> updateStateIfData { oldState ->
oldState.copy( oldState.copy(
error = error.message error = error.message
@ -46,10 +83,15 @@ class AuthViewModel : ViewModel() {
) )
} }
} }
}
is AuthIntent.TextInput -> { is AuthIntent.TextInput -> {
updateStateIfData { oldState -> updateStateIfData { oldState ->
oldState.copy( oldState.copy(
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text), isEnabledSend = checkCodeFormatUseCase.invoke(
login = intent.login,
password = intent.password
),
error = null error = null
) )
} }