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:
@@ -82,6 +82,9 @@ dependencies {
|
|||||||
// [DEPENDENCY] Logging
|
// [DEPENDENCY] Logging
|
||||||
implementation(Libs.timber)
|
implementation(Libs.timber)
|
||||||
|
|
||||||
|
// [DEPENDENCY] Security
|
||||||
|
implementation(Libs.securityCrypto)
|
||||||
|
|
||||||
// [DEPENDENCY] Testing
|
// [DEPENDENCY] Testing
|
||||||
testImplementation(Libs.junit)
|
testImplementation(Libs.junit)
|
||||||
androidTestImplementation(Libs.extJunit)
|
androidTestImplementation(Libs.extJunit)
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
|
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
|
||||||
|
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||||
|
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||||
|
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||||
|
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||||
|
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||||
|
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||||
|
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [CORE-LOGIC]
|
||||||
/**
|
/**
|
||||||
@@ -19,12 +26,36 @@ fun NavGraph() {
|
|||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Dashboard.route
|
startDestination = Screen.Setup.route
|
||||||
) {
|
) {
|
||||||
|
composable(route = Screen.Setup.route) {
|
||||||
|
SetupScreen(onSetupComplete = {
|
||||||
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
|
popUpTo(Screen.Setup.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
composable(route = Screen.Dashboard.route) {
|
composable(route = Screen.Dashboard.route) {
|
||||||
DashboardScreen()
|
DashboardScreen()
|
||||||
}
|
}
|
||||||
// TODO: Добавить остальные экраны в граф навигации
|
composable(route = Screen.InventoryList.route) {
|
||||||
|
InventoryListScreen()
|
||||||
|
}
|
||||||
|
composable(route = Screen.ItemDetails.route) {
|
||||||
|
ItemDetailsScreen()
|
||||||
|
}
|
||||||
|
composable(route = Screen.ItemEdit.route) {
|
||||||
|
ItemEditScreen()
|
||||||
|
}
|
||||||
|
composable(route = Screen.LabelsList.route) {
|
||||||
|
LabelsListScreen()
|
||||||
|
}
|
||||||
|
composable(route = Screen.LocationsList.route) {
|
||||||
|
LocationsListScreen()
|
||||||
|
}
|
||||||
|
composable(route = Screen.Search.route) {
|
||||||
|
SearchScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_FILE_NavGraph.kt]
|
// [END_FILE_NavGraph.kt]
|
||||||
@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
|
|||||||
* @property route Строковый идентификатор маршрута.
|
* @property route Строковый идентификатор маршрута.
|
||||||
*/
|
*/
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
/**
|
data object Setup : Screen("setup_screen")
|
||||||
* [CONTRACT]
|
|
||||||
* Представляет экран "Дэшборд".
|
|
||||||
*/
|
|
||||||
data object Dashboard : Screen("dashboard_screen")
|
data object Dashboard : Screen("dashboard_screen")
|
||||||
|
data object InventoryList : Screen("inventory_list_screen")
|
||||||
// TODO: Добавить объекты для остальных экранов:
|
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||||
// data object ItemDetails : Screen("item_details_screen")
|
fun createRoute(itemId: String) = "item_details_screen/$itemId"
|
||||||
// data object Search : Screen("search_screen")
|
}
|
||||||
|
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||||
|
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
|
||||||
|
}
|
||||||
|
data object LabelsList : Screen("labels_list_screen")
|
||||||
|
data object LocationsList : Screen("locations_list_screen")
|
||||||
|
data object Search : Screen("search_screen")
|
||||||
}
|
}
|
||||||
// [END_FILE_Screen.kt]
|
// [END_FILE_Screen.kt]
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||||
|
// [FILE] SetupScreen.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
||||||
|
// [ENTRYPOINT]
|
||||||
|
@Composable
|
||||||
|
fun SetupScreen(
|
||||||
|
viewModel: SetupViewModel = hiltViewModel(),
|
||||||
|
onSetupComplete: () -> Unit
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
if (uiState.isSetupComplete) {
|
||||||
|
onSetupComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupScreenContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onServerUrlChange = viewModel::onServerUrlChange,
|
||||||
|
onUsernameChange = viewModel::onUsernameChange,
|
||||||
|
onPasswordChange = viewModel::onPasswordChange,
|
||||||
|
onConnectClick = viewModel::connect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [CONTENT]
|
||||||
|
@Composable
|
||||||
|
private fun SetupScreenContent(
|
||||||
|
uiState: SetupUiState,
|
||||||
|
onServerUrlChange: (String) -> Unit,
|
||||||
|
onUsernameChange: (String) -> Unit,
|
||||||
|
onPasswordChange: (String) -> Unit,
|
||||||
|
onConnectClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(title = { Text("Server Setup") })
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.serverUrl,
|
||||||
|
onValueChange = onServerUrlChange,
|
||||||
|
label = { Text("Server URL") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.username,
|
||||||
|
onValueChange = onUsernameChange,
|
||||||
|
label = { Text("Username") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.password,
|
||||||
|
onValueChange = onPasswordChange,
|
||||||
|
label = { Text("Password") },
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
onClick = onConnectClick,
|
||||||
|
enabled = !uiState.isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
} else {
|
||||||
|
Text("Connect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState.error?.let {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun SetupScreenPreview() {
|
||||||
|
SetupScreenContent(
|
||||||
|
uiState = SetupUiState(error = "Failed to connect"),
|
||||||
|
onServerUrlChange = {},
|
||||||
|
onUsernameChange = {},
|
||||||
|
onPasswordChange = {},
|
||||||
|
onConnectClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_FILE_SetupScreen.kt]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||||
|
// [FILE] SetupUiState.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
|
// [STATE]
|
||||||
|
data class SetupUiState(
|
||||||
|
val serverUrl: String = "",
|
||||||
|
val username: String = "",
|
||||||
|
val password: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val isSetupComplete: Boolean = false
|
||||||
|
)
|
||||||
|
// [END_FILE_SetupUiState.kt]
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||||
|
// [FILE] SetupViewModel.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.homebox.lens.domain.model.Credentials
|
||||||
|
import com.homebox.lens.domain.model.Result
|
||||||
|
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||||
|
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// [VIEWMODEL]
|
||||||
|
@HiltViewModel
|
||||||
|
class SetupViewModel @Inject constructor(
|
||||||
|
private val credentialsRepository: CredentialsRepository,
|
||||||
|
private val loginUseCase: LoginUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// [STATE]
|
||||||
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCredentials() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
credentialsRepository.getCredentials().collect { credentials ->
|
||||||
|
if (credentials != null) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
serverUrl = credentials.serverUrl,
|
||||||
|
username = credentials.username,
|
||||||
|
password = credentials.password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ACTION]
|
||||||
|
fun onServerUrlChange(newUrl: String) {
|
||||||
|
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ACTION]
|
||||||
|
fun onUsernameChange(newUsername: String) {
|
||||||
|
_uiState.update { it.copy(username = newUsername) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ACTION]
|
||||||
|
fun onPasswordChange(newPassword: String) {
|
||||||
|
_uiState.update { it.copy(password = newPassword) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ACTION]
|
||||||
|
fun connect() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
val credentials = Credentials(
|
||||||
|
serverUrl = _uiState.value.serverUrl.trim(),
|
||||||
|
username = _uiState.value.username.trim(),
|
||||||
|
password = _uiState.value.password
|
||||||
|
)
|
||||||
|
|
||||||
|
credentialsRepository.saveCredentials(credentials)
|
||||||
|
|
||||||
|
when (val result = loginUseCase(credentials)) {
|
||||||
|
is Result.Success -> {
|
||||||
|
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
_uiState.update { it.copy(isLoading = false, error = result.exception.message ?: "Login failed") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_FILE_SetupViewModel.kt]
|
||||||
@@ -94,6 +94,9 @@ object Libs {
|
|||||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
|
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
|
||||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
|
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
|
||||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
||||||
|
|
||||||
|
// Security
|
||||||
|
const val securityCrypto = "androidx.security:security-crypto:${Versions.securityCrypto}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// [END_FILE_Dependencies.kt]
|
// [END_FILE_Dependencies.kt]
|
||||||
|
|||||||
@@ -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.ItemUpdateDto
|
||||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
import com.homebox.lens.data.api.dto.LabelOutDto
|
||||||
import com.homebox.lens.data.api.dto.LocationOutCountDto
|
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.PaginationResultDto
|
||||||
|
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
@@ -27,6 +29,10 @@ import retrofit2.http.Query
|
|||||||
*/
|
*/
|
||||||
interface HomeboxApiService {
|
interface HomeboxApiService {
|
||||||
|
|
||||||
|
// [ENDPOINT] Auth
|
||||||
|
@POST("v1/users/login")
|
||||||
|
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||||
|
|
||||||
// [ENDPOINT] Items
|
// [ENDPOINT] Items
|
||||||
@GET("v1/items")
|
@GET("v1/items")
|
||||||
suspend fun getItems(
|
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]
|
// [PROVIDER]
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@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
|
// [ACTION] Build Retrofit instance
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
.addConverterFactory(moshiConverterFactory)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
package com.homebox.lens.data.di
|
package com.homebox.lens.data.di
|
||||||
|
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
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 com.homebox.lens.domain.repository.ItemRepository
|
||||||
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -15,18 +17,23 @@ import javax.inject.Singleton
|
|||||||
// [CONTRACT]
|
// [CONTRACT]
|
||||||
/**
|
/**
|
||||||
* [MODULE: DaggerHilt('RepositoryModule')]
|
* [MODULE: DaggerHilt('RepositoryModule')]
|
||||||
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
|
* [PURPOSE] Предоставляет реализации для интерфейсов репозиториев.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object RepositoryModule {
|
abstract class RepositoryModule {
|
||||||
|
|
||||||
// [PROVIDER]
|
@Binds
|
||||||
@Provides
|
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository {
|
abstract fun bindItemRepository(itemRepositoryImpl: com.homebox.lens.data.repository.ItemRepositoryImpl): ItemRepository
|
||||||
return ItemRepositoryImpl(apiService)
|
|
||||||
}
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCredentialsRepository(credentialsRepositoryImpl: CredentialsRepositoryImpl): CredentialsRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// [END_FILE_RepositoryModule.kt]
|
// [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
|
package com.homebox.lens.data.repository
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
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.toDomain
|
||||||
import com.homebox.lens.data.api.dto.toDto
|
import com.homebox.lens.data.api.dto.toDto
|
||||||
import com.homebox.lens.domain.model.*
|
import com.homebox.lens.domain.model.*
|
||||||
|
import com.homebox.lens.domain.repository.AuthRepository
|
||||||
import com.homebox.lens.domain.repository.ItemRepository
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -19,9 +24,29 @@ import javax.inject.Singleton
|
|||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class ItemRepositoryImpl @Inject constructor(
|
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 {
|
) : 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
|
* [CONTRACT] @see ItemRepository.createItem
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
|
// [FILE] Credentials.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Data class to hold server credentials.
|
||||||
|
* @property serverUrl The URL of the Homebox server.
|
||||||
|
* @property username The username for authentication.
|
||||||
|
* @property password The password for authentication.
|
||||||
|
*/
|
||||||
|
data class Credentials(
|
||||||
|
val serverUrl: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
// [END_FILE_Credentials.kt]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.domain.repository
|
||||||
|
// [FILE] AuthRepository.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.domain.repository
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Repository for managing authentication tokens.
|
||||||
|
*/
|
||||||
|
interface AuthRepository {
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Saves the authentication token.
|
||||||
|
* @param token The token to save.
|
||||||
|
*/
|
||||||
|
suspend fun saveToken(token: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Retrieves the authentication token.
|
||||||
|
* @return A Flow emitting the token, or null if not found.
|
||||||
|
*/
|
||||||
|
fun getToken(): Flow<String?>
|
||||||
|
}
|
||||||
|
// [END_FILE_AuthRepository.kt]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.domain.repository
|
||||||
|
// [FILE] CredentialsRepository.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.domain.repository
|
||||||
|
|
||||||
|
import com.homebox.lens.domain.model.Credentials
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Repository for managing user credentials.
|
||||||
|
*/
|
||||||
|
interface CredentialsRepository {
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Saves the user credentials securely.
|
||||||
|
* @param credentials The credentials to save.
|
||||||
|
*/
|
||||||
|
suspend fun saveCredentials(credentials: Credentials)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Retrieves the saved user credentials.
|
||||||
|
* @return A Flow emitting the saved [Credentials], or null if none are saved.
|
||||||
|
*/
|
||||||
|
fun getCredentials(): Flow<Credentials?>
|
||||||
|
}
|
||||||
|
// [END_FILE_CredentialsRepository.kt]
|
||||||
@@ -12,6 +12,7 @@ import com.homebox.lens.domain.model.*
|
|||||||
* Определяет контракт, которому должен следовать слой данных.
|
* Определяет контракт, которому должен следовать слой данных.
|
||||||
*/
|
*/
|
||||||
interface ItemRepository {
|
interface ItemRepository {
|
||||||
|
suspend fun login(credentials: Credentials): Result<Unit>
|
||||||
suspend fun createItem(newItemData: ItemCreate): ItemSummary
|
suspend fun createItem(newItemData: ItemCreate): ItemSummary
|
||||||
suspend fun getItemDetails(itemId: String): ItemOut
|
suspend fun getItemDetails(itemId: String): ItemOut
|
||||||
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
|
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||||
|
// [FILE] LoginUseCase.kt
|
||||||
|
|
||||||
|
package com.homebox.lens.domain.usecase
|
||||||
|
|
||||||
|
import com.homebox.lens.domain.model.Credentials
|
||||||
|
import com.homebox.lens.domain.repository.ItemRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import com.homebox.lens.domain.model.Result
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Use case for user login.
|
||||||
|
* @param itemRepository The repository to handle item and auth operations.
|
||||||
|
*/
|
||||||
|
class LoginUseCase @Inject constructor(
|
||||||
|
private val itemRepository: ItemRepository
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Executes the login process.
|
||||||
|
* @param credentials The user's credentials.
|
||||||
|
* @return A [Result] object indicating success or failure.
|
||||||
|
*/
|
||||||
|
suspend operator fun invoke(credentials: Credentials): Result<Unit> {
|
||||||
|
return itemRepository.login(credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_FILE_LoginUseCase.kt]
|
||||||
@@ -59,6 +59,15 @@
|
|||||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search">
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search">
|
||||||
<purpose_summary>ViewModel for the Search screen.</purpose_summary>
|
<purpose_summary>ViewModel for the Search screen.</purpose_summary>
|
||||||
</file>
|
</file>
|
||||||
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" status="stub" spec_ref_id="screen_setup">
|
||||||
|
<purpose_summary>UI for the Setup screen.</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt" status="stub" spec_ref_id="screen_setup">
|
||||||
|
<purpose_summary>ViewModel for the Setup screen.</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt" status="implemented" spec_ref_id="screen_setup">
|
||||||
|
<purpose_summary>UI state for the Setup screen.</purpose_summary>
|
||||||
|
</file>
|
||||||
</module>
|
</module>
|
||||||
<module name="data" type="android_library">
|
<module name="data" type="android_library">
|
||||||
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary>
|
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary>
|
||||||
@@ -80,12 +89,33 @@
|
|||||||
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo">
|
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo">
|
||||||
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary>
|
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary>
|
||||||
</file>
|
</file>
|
||||||
|
<file name="data/src/main/java/com/homebox/lens/data/di/StorageModule.kt" status="implemented" ref_id="di_storage">
|
||||||
|
<purpose_summary>Hilt module for providing storage-related dependencies (EncryptedSharedPreferences).</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt" status="implemented" ref_id="repo_credentials_impl">
|
||||||
|
<purpose_summary>Implementation of the CredentialsRepository.</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt" status="implemented" ref_id="repo_auth_impl">
|
||||||
|
<purpose_summary>Implementation of the AuthRepository.</purpose_summary>
|
||||||
|
</file>
|
||||||
</module>
|
</module>
|
||||||
<module name="domain" type="kotlin_jvm_library">
|
<module name="domain" type="kotlin_jvm_library">
|
||||||
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary>
|
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary>
|
||||||
|
<file name="domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt" status="implemented" ref_id="model_credentials">
|
||||||
|
<purpose_summary>Data class for holding user credentials.</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt" status="implemented" ref_id="repo_auth_interface">
|
||||||
|
<purpose_summary>Interface for the auth repository.</purpose_summary>
|
||||||
|
</file>
|
||||||
|
<file name="domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt" status="implemented" ref_id="repo_credentials_interface">
|
||||||
|
<purpose_summary>Interface for the credentials repository.</purpose_summary>
|
||||||
|
</file>
|
||||||
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface">
|
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface">
|
||||||
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary>
|
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary>
|
||||||
</file>
|
</file>
|
||||||
|
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" status="implemented" spec_ref_id="uc_login">
|
||||||
|
<purpose_summary>Use case for user login.</purpose_summary>
|
||||||
|
</file>
|
||||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item">
|
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item">
|
||||||
<purpose_summary>Use case for creating a new item.</purpose_summary>
|
<purpose_summary>Use case for creating a new item.</purpose_summary>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
|
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
|
||||||
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
|
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
|
||||||
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
|
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
|
||||||
|
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
|
||||||
|
|
||||||
<!-- UI Screens -->
|
<!-- UI Screens -->
|
||||||
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
|
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
|
||||||
@@ -126,5 +127,6 @@
|
|||||||
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
|
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
|
||||||
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
|
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
|
||||||
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
|
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
|
||||||
|
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
|
||||||
</IMPLEMENTATION_MAP>
|
</IMPLEMENTATION_MAP>
|
||||||
</PROJECT_SPECIFICATION>
|
</PROJECT_SPECIFICATION>
|
||||||
Reference in New Issue
Block a user