feat: Implement setup screen and login logic
- Add SetupScreen with UI for server URL, username, and password input. - Make SetupScreen the initial screen in the navigation graph. - Implement secure credential storage using EncryptedSharedPreferences. - Create CredentialsRepository and AuthRepository to manage credentials and auth tokens. - Add LoginUseCase to handle the business logic for logging in. - Implement a temporary Retrofit client in ItemRepository to handle login against a user-provided URL. - Integrate login logic into SetupViewModel. - Update all relevant project documentation and DI modules.
This commit is contained in:
@@ -10,7 +10,9 @@ import com.homebox.lens.data.api.dto.ItemSummaryDto
|
||||
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
||||
import com.homebox.lens.data.api.dto.LocationOutCountDto
|
||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||
import com.homebox.lens.data.api.dto.PaginationResultDto
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
@@ -27,6 +29,10 @@ import retrofit2.http.Query
|
||||
*/
|
||||
interface HomeboxApiService {
|
||||
|
||||
// [ENDPOINT] Auth
|
||||
@POST("v1/users/login")
|
||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||
|
||||
// [ENDPOINT] Items
|
||||
@GET("v1/items")
|
||||
suspend fun getItems(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LoginFormDto.kt
|
||||
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LoginFormDto(
|
||||
@Json(name = "username") val username: String,
|
||||
@Json(name = "password") val password: String,
|
||||
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
||||
)
|
||||
// [END_FILE_LoginFormDto.kt]
|
||||
@@ -0,0 +1,15 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] TokenResponseDto.kt
|
||||
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenResponseDto(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "attachmentToken") val attachmentToken: String,
|
||||
@Json(name = "expiresAt") val expiresAt: String
|
||||
)
|
||||
// [END_FILE_TokenResponseDto.kt]
|
||||
@@ -56,12 +56,19 @@ object ApiModule {
|
||||
// [PROVIDER]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
|
||||
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
// [PROVIDER]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
||||
// [ACTION] Build Retrofit instance
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
||||
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
@@ -15,18 +17,23 @@ import javax.inject.Singleton
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [MODULE: DaggerHilt('RepositoryModule')]
|
||||
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
|
||||
* [PURPOSE] Предоставляет реализации для интерфейсов репозиториев.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RepositoryModule {
|
||||
abstract class RepositoryModule {
|
||||
|
||||
// [PROVIDER]
|
||||
@Provides
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository {
|
||||
return ItemRepositoryImpl(apiService)
|
||||
}
|
||||
abstract fun bindItemRepository(itemRepositoryImpl: com.homebox.lens.data.repository.ItemRepositoryImpl): ItemRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindCredentialsRepository(credentialsRepositoryImpl: CredentialsRepositoryImpl): CredentialsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
|
||||
}
|
||||
|
||||
// [END_FILE_RepositoryModule.kt]
|
||||
|
||||
37
data/src/main/java/com/homebox/lens/data/di/StorageModule.kt
Normal file
37
data/src/main/java/com/homebox/lens/data/di/StorageModule.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] StorageModule.kt
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptedSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"secret_shared_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_FILE_StorageModule.kt]
|
||||
@@ -0,0 +1,30 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] AuthRepositoryImpl.kt
|
||||
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences
|
||||
) : AuthRepository {
|
||||
|
||||
companion object {
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
override suspend fun saveToken(token: String) {
|
||||
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
||||
}
|
||||
|
||||
override fun getToken(): Flow<String?> = flow {
|
||||
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
// [END_FILE_AuthRepositoryImpl.kt]
|
||||
@@ -0,0 +1,46 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] CredentialsRepositoryImpl.kt
|
||||
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
// [REPOSITORY_IMPL]
|
||||
class CredentialsRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences
|
||||
) : CredentialsRepository {
|
||||
|
||||
companion object {
|
||||
private const val KEY_SERVER_URL = "key_server_url"
|
||||
private const val KEY_USERNAME = "key_username"
|
||||
private const val KEY_PASSWORD = "key_password"
|
||||
}
|
||||
|
||||
override suspend fun saveCredentials(credentials: Credentials) {
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
||||
.putString(KEY_USERNAME, credentials.username)
|
||||
.putString(KEY_PASSWORD, credentials.password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun getCredentials(): Flow<Credentials?> = flow {
|
||||
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
||||
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
||||
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
||||
|
||||
if (serverUrl != null && username != null && password != null) {
|
||||
emit(Credentials(serverUrl, username, password))
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
@@ -4,10 +4,15 @@
|
||||
package com.homebox.lens.data.repository
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||
import com.homebox.lens.data.api.dto.toDomain
|
||||
import com.homebox.lens.data.api.dto.toDto
|
||||
import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -19,9 +24,29 @@ import javax.inject.Singleton
|
||||
*/
|
||||
@Singleton
|
||||
class ItemRepositoryImpl @Inject constructor(
|
||||
private val apiService: HomeboxApiService
|
||||
private val apiService: HomeboxApiService,
|
||||
private val authRepository: AuthRepository,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val moshiConverterFactory: MoshiConverterFactory
|
||||
) : ItemRepository {
|
||||
|
||||
override suspend fun login(credentials: Credentials): Result<Unit> {
|
||||
return try {
|
||||
val tempApiService = Retrofit.Builder()
|
||||
.baseUrl(credentials.serverUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
.create(HomeboxApiService::class.java)
|
||||
|
||||
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
||||
val tokenResponse = tempApiService.login(loginForm)
|
||||
authRepository.saveToken(tokenResponse.token)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.createItem
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user