Add full Basic authorization and sync with backend-server

This commit is contained in:
Denis Oleynik 2025-02-19 17:45:05 +03:00
parent 60a7e7544e
commit 72b3324821
15 changed files with 64 additions and 47 deletions

View File

@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
class LoginDto( class LoginDto(
@SerializedName("username") @SerializedName("username")
val username: String?, val username: String,
@SerializedName("password") @SerializedName("password")
val password: String? val password: String
) )

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.data.dto
enum class Role {
ADMIN, USER
}

View File

@ -3,12 +3,20 @@ package ru.myitschool.work.data.dto
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
class UserInfoDto( class UserInfoDto(
@SerializedName("id")
val id: Int,
@SerializedName("name") @SerializedName("name")
val fullname: String?, val fullname: String,
@SerializedName("login")
val login: String,
@SerializedName("role")
val role: Role,
@SerializedName("blocked")
val blocked: Boolean,
@SerializedName("photo") @SerializedName("photo")
val imageUrl: String?, val imageUrl: String,
@SerializedName("position") @SerializedName("position")
val position: String?, val position: String,
@SerializedName("lastVisit") @SerializedName("lastVisit")
val lastEntry: String?, val lastEntry: String,
) )

View File

@ -15,18 +15,18 @@ class AccountRepositoryImpl @Inject constructor(
private val accountNetworkDataSource: AccountNetworkDataSource, private val accountNetworkDataSource: AccountNetworkDataSource,
private val userInfoMapper: Lazy<UserInfoMapper>, private val userInfoMapper: Lazy<UserInfoMapper>,
): UserInfoRepository { ): UserInfoRepository {
override suspend fun getInfo(username: String): Result<UserInfoEntity> { override suspend fun getInfo(basicAuth: String): Result<UserInfoEntity> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
accountNetworkDataSource.getInfo(username).fold( accountNetworkDataSource.getInfo(basicAuth).fold(
onSuccess = { value -> userInfoMapper.get().invoke(value) }, onSuccess = { value -> userInfoMapper.get().invoke(value) },
onFailure = { error -> Result.failure(error) } onFailure = { error -> Result.failure(error) }
) )
} }
} }
override suspend fun openByQr(username: String, content: String): Result<Unit> { override suspend fun openByQr(basicAuth: String, content: String): Result<Unit> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
accountNetworkDataSource.openByQr(username, content) accountNetworkDataSource.openByQr(basicAuth, content)
} }
} }
} }

View File

@ -1,5 +1,6 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import android.util.Base64
import dagger.Reusable import dagger.Reusable
import ru.myitschool.work.data.source.AuthorizationNetworkDataSource import ru.myitschool.work.data.source.AuthorizationNetworkDataSource
import ru.myitschool.work.data.source.AuthorizationStorageDataSource import ru.myitschool.work.data.source.AuthorizationStorageDataSource
@ -19,19 +20,21 @@ class AuthorizationRepositoryImpl @Inject constructor(
override suspend fun login(data: LoginDto): Result<Unit> { override suspend fun login(data: LoginDto): Result<Unit> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
authorizationNetworkDataSource.get().checkLogin(data.username!!) val basicAuthToken = Base64.encodeToString("${data.username}:${data.password}".toByteArray(), Base64.NO_WRAP)
authorizationNetworkDataSource.get().checkAuthCredentials(basicAuthToken)
.onSuccess { .onSuccess {
authorizationStorageDataSource.get().updateLogin(data.username) authorizationStorageDataSource.get().updateBasicAuth(basicAuthToken)
} }
} }
} }
override suspend fun logout() { override suspend fun logout() {
authorizationStorageDataSource.get().updateLogin(null) authorizationStorageDataSource.get().updateBasicAuth(null)
} }
override suspend fun getLogin(): Result<String> { override suspend fun getLogin(): Result<String> {
val result = authorizationStorageDataSource.get().login.firstOrNull() val result = authorizationStorageDataSource.get().basicAuthToken.firstOrNull()
return if (result == null) { return if (result == null) {
Result.failure(Exception("Not authorize")) Result.failure(Exception("Not authorize"))
} else { } else {

View File

@ -2,20 +2,21 @@ package ru.myitschool.work.data.source
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.PATCH import retrofit2.http.PATCH
import retrofit2.http.Path import retrofit2.http.Path
import ru.myitschool.work.data.dto.OpenQrDto import ru.myitschool.work.data.dto.OpenQrDto
import ru.myitschool.work.data.dto.UserInfoDto import ru.myitschool.work.data.dto.UserInfoDto
interface AccountApi { interface AccountApi {
@GET("api/{username}/info") @GET("api/employee/info")
suspend fun getInfo( suspend fun getInfo(
@Path("username") username: String @Header("Authorization") basicAuth: String
) : UserInfoDto ) : UserInfoDto
@PATCH("api/{username}/open") @PATCH("api/employee/open")
suspend fun openByQr( suspend fun openByQr(
@Path("username") username: String, @Header("Authorization") basicAuth: String,
@Body content: OpenQrDto @Body content: OpenQrDto
) )
} }

View File

@ -14,14 +14,14 @@ class AccountNetworkDataSource @Inject constructor(
retrofit.create(AccountApi::class.java) retrofit.create(AccountApi::class.java)
} }
suspend fun getInfo(username: String): Result<UserInfoDto> { suspend fun getInfo(basicAuth: String): Result<UserInfoDto> {
return kotlin.runCatching { api.getInfo(username = username) } return kotlin.runCatching { api.getInfo(basicAuth = basicAuth) }
} }
suspend fun openByQr(username: String, content: String): Result<Unit> { suspend fun openByQr(basicAuth: String, content: String): Result<Unit> {
return kotlin.runCatching { return kotlin.runCatching {
api.openByQr( api.openByQr(
username = username, basicAuth = basicAuth,
content = OpenQrDto(value = content) content = OpenQrDto(value = content)
) )
} }

View File

@ -1,19 +1,14 @@
package ru.myitschool.work.data.source package ru.myitschool.work.data.source
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
interface AuthorizationApi { interface AuthorizationApi {
@GET("api/{username}/auth") @POST("api/login")
suspend fun checkLogin(
@Path("username") username: String
) : Response<Unit>
@GET("api/login")
suspend fun login( suspend fun login(
@Body content: AccountApi @Header("Authorization") basicAuthorization: String,
): Response<Unit> ): Response<Unit>
} }

View File

@ -13,8 +13,8 @@ class AuthorizationNetworkDataSource @Inject constructor(
retrofit.create(AuthorizationApi::class.java) retrofit.create(AuthorizationApi::class.java)
} }
suspend fun checkLogin(username: String): Result<Unit> { suspend fun checkAuthCredentials(basicAuth: String): Result<Unit> {
return kotlin.runCatching { api.checkLogin(username = username) }.fold( return kotlin.runCatching { api.login("Basic $basicAuth") }.fold(
onSuccess = { response -> onSuccess = { response ->
when (response.code()) { when (response.code()) {
200 -> Result.success(Unit) 200 -> Result.success(Unit)

View File

@ -17,22 +17,22 @@ class AuthorizationStorageDataSource @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) { ) {
private val Context.storage: DataStore<Preferences> by preferencesDataStore(name = NAME) private val Context.storage: DataStore<Preferences> by preferencesDataStore(name = NAME)
val login: Flow<String?> = context.storage.data.map { preferences -> val basicAuthToken: Flow<String?> = context.storage.data.map { preferences ->
preferences[LOGIN_KEY] preferences[TOKEN_KEY]
} }
suspend fun updateLogin(username: String?) { suspend fun updateBasicAuth(token: String?) {
context.storage.edit { settings -> context.storage.edit { settings ->
if (username != null) { if (token != null) {
settings[LOGIN_KEY] = username settings[TOKEN_KEY] = "Basic $token"
} else { } else {
settings.remove(LOGIN_KEY) settings.remove(TOKEN_KEY)
} }
} }
} }
private companion object { private companion object {
const val NAME = "auth_data" const val NAME = "auth_data"
val LOGIN_KEY = stringPreferencesKey("login") val TOKEN_KEY = stringPreferencesKey("basicAuthToken")
} }
} }

View File

@ -11,7 +11,7 @@ class GetUserInfoUseCase @Inject constructor(
) { ) {
suspend operator fun invoke(): Result<UserInfoEntity> { suspend operator fun invoke(): Result<UserInfoEntity> {
return getLoginUseCase().fold( return getLoginUseCase().fold(
onSuccess = { username -> repo.getInfo(username = username) }, onSuccess = { basicAuth -> repo.getInfo(basicAuth) },
onFailure = { error -> Result.failure(error) } onFailure = { error -> Result.failure(error) }
) )
} }

View File

@ -10,7 +10,7 @@ class OpenByQrUseCase @Inject constructor(
) { ) {
suspend operator fun invoke(content: String): Result<Unit> { suspend operator fun invoke(content: String): Result<Unit> {
return getLoginUseCase().fold( return getLoginUseCase().fold(
onSuccess = { username -> repo.openByQr(username = username, content = content) }, onSuccess = { basicAuth -> repo.openByQr(basicAuth = basicAuth, content = content) },
onFailure = { error -> Result.failure(error) } onFailure = { error -> Result.failure(error) }
) )
} }

View File

@ -3,7 +3,7 @@ package ru.myitschool.work.domain.profile.repo
import ru.myitschool.work.domain.profile.entities.UserInfoEntity import ru.myitschool.work.domain.profile.entities.UserInfoEntity
interface UserInfoRepository { interface UserInfoRepository {
suspend fun getInfo(username: String) : Result<UserInfoEntity> suspend fun getInfo(basicAuth: String) : Result<UserInfoEntity>
suspend fun openByQr(username: String, content: String) : Result<Unit> suspend fun openByQr(basicAuth: String, content: String) : Result<Unit>
} }

View File

@ -64,6 +64,11 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
viewModel.inputLogin(s.toString()) viewModel.inputLogin(s.toString())
} }
}) })
binding.password.addTextChangedListener(object : TextChangedListener() {
override fun afterTextChanged(s: Editable?) {
viewModel.inputPassword(s.toString())
}
})
} }

View File

@ -32,12 +32,12 @@ class LoginViewModel @Inject constructor(
val state = _state.asStateFlow() val state = _state.asStateFlow()
private var login: String = "" private var login: String = ""
private var password: String = "123"//пока пароль по дефалту private var password: String = ""
fun clickLogin() { fun clickLogin() {
viewModelScope.launch { viewModelScope.launch {
_state.update { State.Loading } _state.update { State.Loading }
val data = LoginDto(login,password) val data = LoginDto(login, password)
loginUseCase.get().invoke(data).fold( loginUseCase.get().invoke(data).fold(
onSuccess = { onSuccess = {
_action.emit(Action.OpenProfile) _action.emit(Action.OpenProfile)
@ -56,7 +56,7 @@ class LoginViewModel @Inject constructor(
//Чек пароля на правильность // Чек пароля на правильность
fun inputLogin(login: String) { fun inputLogin(login: String) {
this.login = login this.login = login
viewModelScope.launch { viewModelScope.launch {
@ -67,7 +67,7 @@ class LoginViewModel @Inject constructor(
} }
} }
} }
fun passwordCheck(password: String){ fun inputPassword(password: String){
this.password = password this.password = password
} }