diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a28d464..1421b3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,11 +47,18 @@ dependencies { implementation(Dependencies.Retrofit.library) implementation(Dependencies.Retrofit.gsonConverter) + + implementation("com.squareup.picasso:picasso:2.8") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("androidx.datastore:datastore-preferences:1.1.1") implementation("com.google.mlkit:barcode-scanning:17.3.0") + implementation ("io.ktor:ktor-client-core:2.3.5") + implementation ("io.ktor:ktor-client-cio:2.3.5") + implementation ("io.ktor:ktor-client-content-negotiation:2.3.5") + implementation ("io.ktor:ktor-serialization-kotlinx-json:2.3.5") + val cameraX = "1.3.4" implementation("androidx.camera:camera-core:$cameraX") implementation("androidx.camera:camera-camera2:$cameraX") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a986978..8f78226 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,15 +19,23 @@ android:supportsRtl="true" android:theme="@style/Theme.Default" tools:targetApi="31"> + + + + + + - + \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/auth/AuthNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/auth/AuthNetworkDataSource.kt new file mode 100644 index 0000000..b8b7bc7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/auth/AuthNetworkDataSource.kt @@ -0,0 +1,44 @@ +package ru.myitschool.work.data.auth + +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.myitschool.work.core.Constants.SERVER_ADDRESS +import ru.myitschool.work.data.auth.Network.client +import ru.myitschool.work.data.user.UserDto + +object AuthNetworkDataSource { + + suspend fun isUserExist(login: String): Result = withContext(Dispatchers.IO) { + runCatching { + val result = client.get("$SERVER_ADDRESS/api/user/login/$login") //10.0.2.2 + when (result.status) { + HttpStatusCode.OK -> { return@runCatching true } + HttpStatusCode.NotFound -> { return@runCatching false } + else -> {return@runCatching null } + } + } + } + + suspend fun login(token: String): Result = withContext(Dispatchers.IO) { + runCatching { + val result = client.get("$SERVER_ADDRESS/api/user/login") { + header(HttpHeaders.Authorization, token) + } + if (result.status == HttpStatusCode.Unauthorized) { + error("Неверный email или пароль") + } + result.body() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/auth/AuthRepoImpl.kt b/app/src/main/java/ru/myitschool/work/data/auth/AuthRepoImpl.kt new file mode 100644 index 0000000..b5b6952 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/auth/AuthRepoImpl.kt @@ -0,0 +1,27 @@ +package ru.myitschool.work.data.auth + +import ru.myitschool.work.data.user.UserDto +import ru.sicampus.bootcamp2025.domain.auth.AuthRepo + +class AuthRepoImpl( + private val authNetworkDataSource: AuthNetworkDataSource, + private val authStorageDataSource: AuthStorageDataSource +) : AuthRepo{ + override suspend fun isUserExist(email: String): Result { + return authNetworkDataSource.isUserExist(email) + } + + override suspend fun login(email: String, password: String): Result { + val token = authStorageDataSource.updateToken(email, password) + + val userInfo = authNetworkDataSource.login(token).onFailure { + authStorageDataSource.clear() + } + if (userInfo.isSuccess){ + authStorageDataSource.updateUserInfo(userInfo) + } + + return userInfo + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/auth/AuthStorageDataSource.kt b/app/src/main/java/ru/myitschool/work/data/auth/AuthStorageDataSource.kt new file mode 100644 index 0000000..38f2a18 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/auth/AuthStorageDataSource.kt @@ -0,0 +1,30 @@ +package ru.myitschool.work.data.auth + +import okhttp3.Credentials +import ru.myitschool.work.data.user.UserDto + + +object AuthStorageDataSource { + var token: String? = null + private set + + var userInfo : UserDto? = null + fun updateToken(email : String, password : String) : String { + val updateToken = Credentials.basic(email, password) + token = updateToken + return updateToken + } + fun updateUserInfo(userDto: Result) { + userDto.onSuccess { user -> + userInfo = user + }.onFailure { error -> + userInfo = null + error("Server Error id = null") + } + + } + fun clear() { + token = null + userInfo = null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/auth/Network.kt b/app/src/main/java/ru/myitschool/work/data/auth/Network.kt new file mode 100644 index 0000000..86712f4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/auth/Network.kt @@ -0,0 +1,18 @@ +package ru.myitschool.work.data.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +object Network { + val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { + isLenient = true + ignoreUnknownKeys = true + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/user/UserDto.kt b/app/src/main/java/ru/myitschool/work/data/user/UserDto.kt new file mode 100644 index 0000000..a591b96 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/user/UserDto.kt @@ -0,0 +1,39 @@ +package ru.myitschool.work.data.user + +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import ru.myitschool.work.domain.user.UserEntity +import java.sql.Timestamp + +@Serializable +data class UserDto( + @SerialName("id") + val id : Long?, + @SerialName("login") + var login: String, + @SerialName("birthDate") + var birthDate : String?, + @SerialName("name") + var name: String, + @SerialName("avatarUrl") + var avatarUrl: String?, + @SerialName("position") + val position: String, + @SerialName("lastEntry") + val lastEntry : String, + @SerialName("authorities") + val authorities : String +) { + fun toEntity(): UserEntity { + return UserEntity( + id = id ?: throw IllegalArgumentException("User ID cannot be null"), + login, + name = name, + avatarUrl = avatarUrl, + position = position, + lastEntry = lastEntry, + authorities = authorities + ) + } +} diff --git a/app/src/main/java/ru/myitschool/work/domain/user/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/user/UserEntity.kt new file mode 100644 index 0000000..34f7609 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/user/UserEntity.kt @@ -0,0 +1,14 @@ +package ru.myitschool.work.domain.user + +import java.sql.Timestamp + +data class UserEntity( + val id : Long, + var login: String, + var name: String, + var avatarUrl: String?, + val position : String, + var lastEntry : String, + val authorities : String + +) diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt index 88a796a..4cfeab2 100644 --- a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -21,7 +21,7 @@ class RootActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_root) - val navHostFragment = supportFragmentManager + /*val navHostFragment = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? if (navHostFragment != null) { @@ -52,5 +52,6 @@ class RootActivity : AppCompatActivity() { false } return popBackResult || super.onSupportNavigateUp() + }*/ } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/EntryActivity.kt b/app/src/main/java/ru/myitschool/work/ui/login/EntryActivity.kt new file mode 100644 index 0000000..9960af8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/EntryActivity.kt @@ -0,0 +1,20 @@ +package ru.myitschool.work.ui.login + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import dagger.hilt.android.AndroidEntryPoint +import ru.myitschool.work.R + +@AndroidEntryPoint +class EntryActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_entry) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt index 02842ce..725834c 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -1,6 +1,10 @@ package ru.myitschool.work.ui.login +import android.content.Intent import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Patterns import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -12,25 +16,68 @@ import ru.myitschool.work.utils.visibleOrGone @AndroidEntryPoint class LoginFragment : Fragment(R.layout.fragment_login) { - private var _binding: FragmentLoginBinding? = null - private val binding: FragmentLoginBinding get() = _binding!! + private var _viewBinding: FragmentLoginBinding? = null + private val viewBinding: FragmentLoginBinding get() = _viewBinding!! private val viewModel: LoginViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - _binding = FragmentLoginBinding.bind(view) - subscribe() - } + _viewBinding = FragmentLoginBinding.bind(view) - private fun subscribe() { - viewModel.state.collectWhenStarted(this) { state -> - binding.loading.visibleOrGone(state) + + /*viewBinding.signInButton.setOnClickListener { + val login = viewBinding.userLogin.text.toString() + val password = viewBinding.userPassword.text.toString() + if (!isValidEmail(login)) { + viewBinding.errorText.text = getString(R.string.error_valid) + viewBinding.errorText.visibility = View.VISIBLE + } + else if (!isValidPassword(password)) { + viewBinding.errorText.text = getString(R.string.error_valid) + viewBinding.errorText.visibility = View.VISIBLE + } + else { + viewModel.auth(email, password) + viewBinding.errorText.visibility = View.GONE + } } + + viewModel.state.collectWithLifecycle(this) { state -> + if (state is AuthViewModel.State.Show) { + viewBinding.errorText.text = state.errorText.toString() + viewBinding.errorText.visibility = + if (state.errorText == null) View.GONE else View.VISIBLE + } + } + + viewModel.navigateToMain.collectWithLifecycle(viewLifecycleOwner) { userRole -> + val intent = Intent(requireContext(), MainActivity::class.java).apply { + putExtra("USER_ROLE", userRole) + } + startActivity(intent) + requireActivity().finish() + } + + viewBinding.userLogin.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit + override fun afterTextChanged(p0: Editable?) { + + } + + })*/ + } + private fun isValidEmail(email: String): Boolean { + return Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + private fun isValidPassword(password : String) : Boolean { + return password.length >= 8 } override fun onDestroyView() { - _binding = null + _viewBinding = null super.onDestroyView() } + } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt index 3a53d6c..c7edf14 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt @@ -1,17 +1,132 @@ package ru.myitschool.work.ui.login +import LoginUseCase import android.content.Context +import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.myitschool.work.R +import ru.myitschool.work.domain.auth.IsUserExistUseCase import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( @ApplicationContext private val context: Context, + private val isUserExistUseCase: IsUserExistUseCase, + private val loginUseCase: LoginUseCase ) : ViewModel() { private val _state = MutableStateFlow(true) val state = _state.asStateFlow() + + private val _navigateToMain = MutableSharedFlow() + val navigateToMain = _navigateToMain.asSharedFlow() + + private val _userRole = MutableSharedFlow() + val userRole = _userRole.asSharedFlow() + +/* + init { + viewModelScope.launch { + updateState() + } + + } + fun auth( + email : String, + password : String + ) + { + viewModelScope.launch { + _state.emit(State.Loading) + when (checkUserExistence(email)) { + true -> { + loginUser(email, password) + } + false -> { + updateState(context.getString(R.string.error_invalid_credentials)) + } + null -> updateState(context.getString(R.string.error_unknown)) + } + } + } + + private suspend fun checkUserExistence(email: String):Boolean?{ + return try { + val result = isUserExistUseCase(email) + result.fold( + onSuccess = {isExist -> isExist}, + onFailure = { + Log.e("AuthViewModel", "Error checking user existence", it) + null + } + ) + } catch (e: Exception) { + Log.e("AuthViewModel", "Error during user existence check", e) + null + } + } + + + private suspend fun loginUser(email: String, password: String) { + loginUseCase(email, password).fold( + onSuccess = { user -> + println("Login successful") + _userRole.emit(user.authorities) + _navigateToMain.emit(user.authorities) + }, + onFailure = { error -> + updateState(error.message ?: context.getString(R.string.error_unknown)) + } + ) + } + + + private suspend fun updateState(error : String? = null) { + _state.emit(getStateShow(error)) + } + + private fun getStateShow(error : String? = null) : State { + return State.Show( + errorText = error + ) + } + + fun changeLogin() { + viewModelScope.launch { + updateState() + } + } + + sealed interface State { + data object Loading : State + data class Show( + var errorText : String? + ) : State + } +*/ + + /*companion object { + val Factory : ViewModelProvider.Factory = object : ViewModelProvider.Factory { + override fun create(modelClass: KClass, extras: CreationExtras): T { + val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!! + val authRepoImpl = AuthRepoImpl( + authNetworkDataSource = AuthNetworkDataSource, + authStorageDataSource = AuthStorageDataSource + ) + return AuthViewModel( + application = application, + isUserExistUseCase = IsUserExistUseCase(authRepoImpl), + loginUseCase = LoginUseCase(authRepoImpl) + ) as T + } + + } + }*/ } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml new file mode 100644 index 0000000..e4f06c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..06de96f --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/shape_button_additional.xml b/app/src/main/res/drawable/shape_button_additional.xml new file mode 100644 index 0000000..1b2d2a9 --- /dev/null +++ b/app/src/main/res/drawable/shape_button_additional.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_second.xml b/app/src/main/res/drawable/shape_button_second.xml new file mode 100644 index 0000000..1956084 --- /dev/null +++ b/app/src/main/res/drawable/shape_button_second.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_edit_profile.xml b/app/src/main/res/drawable/shape_edit_profile.xml new file mode 100644 index 0000000..8baca05 --- /dev/null +++ b/app/src/main/res/drawable/shape_edit_profile.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_edit_text_login.xml b/app/src/main/res/drawable/shape_edit_text_login.xml new file mode 100644 index 0000000..a59e2fe --- /dev/null +++ b/app/src/main/res/drawable/shape_edit_text_login.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_logout_button.xml b/app/src/main/res/drawable/shape_logout_button.xml new file mode 100644 index 0000000..f18ea8d --- /dev/null +++ b/app/src/main/res/drawable/shape_logout_button.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rounded.xml b/app/src/main/res/drawable/shape_rounded.xml new file mode 100644 index 0000000..6cfef26 --- /dev/null +++ b/app/src/main/res/drawable/shape_rounded.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml new file mode 100644 index 0000000..747b55d --- /dev/null +++ b/app/src/main/res/layout/activity_entry.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index 7f3cd66..cd83595 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -2,15 +2,149 @@ + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/entry_nav_graph.xml b/app/src/main/res/navigation/entry_nav_graph.xml new file mode 100644 index 0000000..926b34c --- /dev/null +++ b/app/src/main/res/navigation/entry_nav_graph.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..3d1a6ab 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,8 @@ #FF018786 #FF000000 #FFFFFFFF + #A7A7A7 + #EBEBEB + #FF6900 + #9F27FE \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b183019..cde5bd7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,11 @@ NTO Pass + Авторизация + Логин + Пароль + Пароль должен содержать не менее 8 символов + Войти + Ошибка валидации\n + Неверный логин или пароль + Непредвиденная ошибка \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 89e63d4..b93b2f2 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,7 @@