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:
2025-08-08 20:17:50 +03:00
parent 01e9b7bb00
commit 2853b5a47e
23 changed files with 602 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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]

View File

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

View File

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

View File

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