diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6c89f7..f0bcb85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,7 +33,7 @@ android { minSdk = 24 targetSdk = 36 versionCode = 1 - versionName = "1.0" + versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index 482e416..d6415c1 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -3,8 +3,10 @@ package com.sampoom.android.app.navigation import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar @@ -12,11 +14,14 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -26,8 +31,9 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.sampoom.android.R import com.sampoom.android.core.ui.theme.backgroundColor -import com.sampoom.android.feature.auth.ui.LoginScreen -import com.sampoom.android.feature.auth.ui.SignUpScreen +import com.sampoom.android.feature.user.ui.AuthViewModel +import com.sampoom.android.feature.user.ui.LoginScreen +import com.sampoom.android.feature.user.ui.SignUpScreen import com.sampoom.android.feature.cart.ui.CartListScreen import com.sampoom.android.feature.order.ui.OrderDetailScreen import com.sampoom.android.feature.order.ui.OrderListScreen @@ -70,9 +76,8 @@ sealed class BottomNavItem( @Composable fun AppNavHost() { val navController = rememberNavController() - - // TODO: 임시 로그인 상태 확인 -> AuthRepository에서 확인하도록 변경 - val isLoggedIn = true + val authViewModel: AuthViewModel = hiltViewModel() + val isLoggedIn by authViewModel.isLoggedIn.collectAsState() NavHost( navController = navController, @@ -82,6 +87,7 @@ fun AppNavHost() { composable(ROUTE_LOGIN) { LoginScreen( onSuccess = { + authViewModel.updateLoginState() navController.navigate(ROUTE_HOME) { popUpTo(ROUTE_LOGIN) { inclusive = true } // 로그인 화면 스택 제거 } @@ -163,7 +169,11 @@ fun MainScreen( composable(ROUTE_DASHBOARD) { DashboardScreen( paddingValues = innerPadding - ) + ) { + parentNavController.navigate(ROUTE_LOGIN) { + popUpTo(0) { inclusive = true } + } + } } composable(ROUTE_OUTBOUND) { OutboundListScreen( @@ -249,8 +259,20 @@ fun BottomNavigationBar(navController: NavHostController) { // 임시 화면들 (실제로는 각각의 feature 모듈에서 구현) @Composable private fun DashboardScreen( - paddingValues: PaddingValues + paddingValues: PaddingValues, + onClick: () -> Unit ) { + val authViewModel: AuthViewModel = hiltViewModel() // 홈 화면 구현 - Text("대시보드 화면", modifier = Modifier.padding(paddingValues)) + Column(Modifier.padding(paddingValues)) { + Text("대시보드 화면", modifier = Modifier.padding(paddingValues)) + + Button(onClick = { + authViewModel.signOut() + onClick() + }) { + Text("로그아웃") + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/database/.gitkeep b/app/src/main/java/com/sampoom/android/core/database/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/core/datastore/AuthPreferences.kt b/app/src/main/java/com/sampoom/android/core/datastore/AuthPreferences.kt new file mode 100644 index 0000000..4c30920 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/datastore/AuthPreferences.kt @@ -0,0 +1,118 @@ +package com.sampoom.android.core.datastore + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.sampoom.android.feature.user.domain.model.User +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +// Per official guidance, DataStore instance should be single and at top-level. +private val Context.authDataStore by preferencesDataStore(name = "auth_prefs") + +@Singleton +class AuthPreferences @Inject constructor( + @param:ApplicationContext private val context: Context, + private val cryptoManager: CryptoManager +){ + private val dataStore = context.authDataStore + + private object Keys { + val ACCESS_TOKEN: Preferences.Key = stringPreferencesKey("access_token") + val REFRESH_TOKEN: Preferences.Key = stringPreferencesKey("refresh_token") + val TOKEN_EXPIRES_AT: Preferences.Key = longPreferencesKey("token_expires_at") + val USER_ID: Preferences.Key = stringPreferencesKey("user_id") + val USER_NAME: Preferences.Key = stringPreferencesKey("user_name") + val USER_ROLE: Preferences.Key = stringPreferencesKey("user_role") + } + + suspend fun saveUser(user: User) { + val expiresAt = System.currentTimeMillis() + (user.expiresIn * 1000) + dataStore.edit { prefs -> + prefs[Keys.ACCESS_TOKEN] = cryptoManager.encrypt(user.accessToken) + prefs[Keys.REFRESH_TOKEN] = cryptoManager.encrypt(user.refreshToken) + prefs[Keys.TOKEN_EXPIRES_AT] = expiresAt + prefs[Keys.USER_ID] = cryptoManager.encrypt(user.userId.toString()) + prefs[Keys.USER_NAME] = cryptoManager.encrypt(user.userName) + prefs[Keys.USER_ROLE] = cryptoManager.encrypt(user.role) + } + } + + suspend fun saveToken(accessToken: String, refreshToken: String, expiresIn: Long) { + val expiresAt = System.currentTimeMillis() + (expiresIn * 1000) + dataStore.edit { prefs -> + prefs[Keys.ACCESS_TOKEN] = cryptoManager.encrypt(accessToken) + prefs[Keys.REFRESH_TOKEN] = cryptoManager.encrypt(refreshToken) + prefs[Keys.TOKEN_EXPIRES_AT] = expiresAt + } + } + + suspend fun getStoredUser(): User? { + val prefs = dataStore.data.first() + val userId = prefs[Keys.USER_ID] + val userName = prefs[Keys.USER_NAME] + val userRole = prefs[Keys.USER_ROLE] + val accessToken = prefs[Keys.ACCESS_TOKEN] + val refreshToken = prefs[Keys.REFRESH_TOKEN] + val expiresAt = prefs[Keys.TOKEN_EXPIRES_AT] + + if (userId != null && userName != null && userRole != null && + accessToken != null && refreshToken != null) { + try { + val remaining = expiresAt?.let { + kotlin.math.max(0L, (it - System.currentTimeMillis()) / 1000) + } ?: 0L + + return User( + cryptoManager.decrypt(userId).toLong(), + cryptoManager.decrypt(userName), + cryptoManager.decrypt(userRole), + cryptoManager.decrypt(accessToken), + cryptoManager.decrypt(refreshToken), + remaining + ) + } catch (e: Exception) { + return null + } + } else return null + } + + suspend fun getAccessToken(): String? { + val encrypted = dataStore.data.first()[Keys.ACCESS_TOKEN] ?: return null + return try { + cryptoManager.decrypt(encrypted) + } catch (e: Exception) { + null + } + } + + suspend fun getRefreshToken(): String? { + val encrypted = dataStore.data.first()[Keys.REFRESH_TOKEN] ?: return null + return try { + cryptoManager.decrypt(encrypted) + } catch (e: Exception) { + null + } + } + + suspend fun isTokenExpired(): Boolean { + val expiresAt = dataStore.data.first()[Keys.TOKEN_EXPIRES_AT] + return expiresAt == null || System.currentTimeMillis() > expiresAt + } + + suspend fun clear() { + dataStore.edit { it.clear() } + } + + suspend fun hasToken(): Boolean { + val accessToken = getAccessToken() + val refreshToken = getRefreshToken() + return !accessToken.isNullOrEmpty() && !refreshToken.isNullOrEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/datastore/CryptoManager.kt b/app/src/main/java/com/sampoom/android/core/datastore/CryptoManager.kt new file mode 100644 index 0000000..36d0704 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/datastore/CryptoManager.kt @@ -0,0 +1,62 @@ +package com.sampoom.android.core.datastore + +import android.content.Context +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import dagger.hilt.android.qualifiers.ApplicationContext +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CryptoManager @Inject constructor( + @param:ApplicationContext private val context: Context +) { + private val keyAlias = "AuthTokenKey" + private val keyStore = KeyStore.getInstance("AndroidKeyStore") + + init { + keyStore.load(null) + createKeyIfNeeded() + } + + private fun createKeyIfNeeded() { + if (!keyStore.containsAlias(keyAlias)) { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + val keyGenParameterSpec = KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(false) + .setRandomizedEncryptionRequired(true) + .build() + keyGenerator.init(keyGenParameterSpec) + keyGenerator.generateKey() + } + } + + fun encrypt(plaintext: String): String { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, keyStore.getKey(keyAlias, null)) + val iv = cipher.iv + val encrypted = cipher.doFinal(plaintext.toByteArray()) + return Base64.encodeToString(iv + encrypted, Base64.DEFAULT) + } + + fun decrypt(encryptedText: String): String { + val encrypted = Base64.decode(encryptedText, Base64.DEFAULT) + val iv = encrypted.sliceArray(0..11) + val ciphertext = encrypted.sliceArray(12 until encrypted.size) + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, keyStore.getKey(keyAlias, null), spec) + return String(cipher.doFinal(ciphertext)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt b/app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt index dac6a14..cf79aeb 100644 --- a/app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt +++ b/app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt @@ -11,26 +11,56 @@ import javax.inject.Singleton import com.google.gson.GsonBuilder import com.google.gson.FieldNamingPolicy import com.sampoom.android.BuildConfig +import com.sampoom.android.core.datastore.AuthPreferences import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit @Module @InstallIn(SingletonComponent::class) object NetworkModule { - @Provides @Singleton fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .addInterceptor(HttpLoggingInterceptor().apply { - level = if (BuildConfig.DEBUG) - HttpLoggingInterceptor.Level.BODY - else - HttpLoggingInterceptor.Level.NONE - }) - // TODO: 로그인 기능 연동 후 인증 인터셉터 추가 필요 - .build() + @Provides + @Singleton + fun provideTokenInterceptor( + authPreferences: AuthPreferences + ): TokenInterceptor { + return TokenInterceptor(authPreferences) + } + + @Provides + @Singleton + fun provideTokenRefreshService( + authPreferences: AuthPreferences + ): TokenRefreshService { + return TokenRefreshService(authPreferences) + } + + @Provides + @Singleton + fun provideOkHttpClient( + tokenInterceptor: TokenInterceptor, + tokenAuthenticator: TokenAuthenticator + ): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor( + HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) + HttpLoggingInterceptor.Level.BODY + else + HttpLoggingInterceptor.Level.NONE + redactHeader("Authorization") // 토큰 비식별화 + redactHeader("Cookie") // 쿠키 비식별화 + } + ) + .addInterceptor(tokenInterceptor) // 토큰 자동 삽입 + .authenticator(tokenAuthenticator) // 토큰 갱신 (Interceptor 대신) + .build() + } - @Provides @Singleton + @Provides + @Singleton fun provideRetrofit(client: OkHttpClient): Retrofit { val gson = GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) diff --git a/app/src/main/java/com/sampoom/android/core/network/TokenAuthenticator.kt b/app/src/main/java/com/sampoom/android/core/network/TokenAuthenticator.kt new file mode 100644 index 0000000..ea4d018 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/network/TokenAuthenticator.kt @@ -0,0 +1,92 @@ +package com.sampoom.android.core.network + +import com.sampoom.android.core.datastore.AuthPreferences +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TokenAuthenticator @Inject constructor( + private val authPreferences: AuthPreferences, + private val tokenRefreshService: TokenRefreshService +) : Authenticator { + private val refreshMutex = Mutex() + + override fun authenticate(route: Route?, response: Response): Request? { + // 이미 재시도된 요청인지 확인 + if (response.request.header("X-Retry-Count") != null) { + return null // 재시도 제한 + } + + return try { + val newUser = runBlocking { + refreshMutex.withLock { + tokenRefreshService.refreshToken().getOrThrow() + } + } + + response.request.newBuilder() + .removeHeader("Authorization") + .addHeader("Authorization", "Bearer ${newUser.accessToken}") + .addHeader("X-Retry-Count", "1") + .build() + } catch (e: retrofit2.HttpException) { + // HTTP 오류별 분기 처리 + when (e.code()) { + 400, 401 -> { + // 인증 실패: 토큰 삭제 + runBlocking { authPreferences.clear() } + null + } + 403 -> { + // 권한 없음: 토큰 삭제 + runBlocking { authPreferences.clear() } + null + } + 429 -> { + // Rate Limit: 토큰 보존, 재시도는 호출자 판단 + null + } + in 500..599 -> { + // 서버 오류: 토큰 보존 + null + } + else -> { + // 기타 HTTP 오류: 토큰 보존 + null + } + } + } catch (e: java.io.IOException) { + // 네트워크 일시 오류: 토큰 보존, 재시도는 호출자 판단 + null + } catch (e: java.net.SocketTimeoutException) { + // 타임아웃: 토큰 보존 + null + } catch (e: java.net.UnknownHostException) { + // DNS 오류: 토큰 보존 + null + } catch (e: java.net.ConnectException) { + // 연결 오류: 토큰 보존 + null + } catch (t: Throwable) { + // 기타 예외: 토큰 보존 + null + } + } + + private suspend fun isTokenExpired(token: String): Boolean { + // 간단한 토큰 만료 체크 (JWT 디코딩 없이) + return try { + val expiresAt = authPreferences.isTokenExpired() + expiresAt + } catch (e: Exception) { + true // 파싱 실패 시 만료로 간주 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/network/TokenInterceptor.kt b/app/src/main/java/com/sampoom/android/core/network/TokenInterceptor.kt new file mode 100644 index 0000000..290573b --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/network/TokenInterceptor.kt @@ -0,0 +1,30 @@ +package com.sampoom.android.core.network + +import com.sampoom.android.core.datastore.AuthPreferences +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class TokenInterceptor @Inject constructor( + private val authPreferences: AuthPreferences +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val existingAuth = originalRequest.header("Authorization") + if (existingAuth.isNullOrBlank()) { + val accessToken = runBlocking { + authPreferences.getAccessToken() + } + if (!accessToken.isNullOrEmpty()) { + val newRequest = originalRequest.newBuilder() + .header("Authorization", "Bearer $accessToken") + .build() + return chain.proceed(newRequest) + } + } + + return chain.proceed(originalRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt new file mode 100644 index 0000000..d95f9dc --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt @@ -0,0 +1,41 @@ +package com.sampoom.android.core.network + +import com.sampoom.android.core.datastore.AuthPreferences +import com.sampoom.android.feature.user.data.remote.api.AuthApi +import com.sampoom.android.feature.user.data.remote.dto.RefreshRequestDto +import com.sampoom.android.feature.user.domain.model.User +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TokenRefreshService @Inject constructor( + private val authPreferences: AuthPreferences +) { + suspend fun refreshToken(): Result = runCatching { + val refreshToken = authPreferences.getRefreshToken() + ?: throw Exception("No refresh token available") + + // 새로운 Retrofit 인스턴스 생성 (인터셉터 없이) + val retrofit = Retrofit.Builder() + .baseUrl("https://sampoom.store/api/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val authApi = retrofit.create(AuthApi::class.java) + val response = authApi.refresh(RefreshRequestDto(refreshToken)) + + val existingUser = authPreferences.getStoredUser() + ?: throw Exception("No user information available") + + val updatedUser = existingUser.copy( + accessToken = response.data.accessToken, + refreshToken = response.data.refreshToken, + expiresIn = response.data.expiresIn + ) + + authPreferences.saveUser(updatedUser) + updatedUser + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt deleted file mode 100644 index 789a143..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sampoom.android.feature.auth.data.local.preferences - -import android.content.Context -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking - -// Per official guidance, DataStore instance should be single and at top-level. -private val Context.authDataStore by preferencesDataStore(name = "auth_prefs") - -@Singleton -class AuthPreferences @Inject constructor( - @param:ApplicationContext private val context: Context -){ - private val dataStore = context.authDataStore - - private object Keys { - val ACCESS_TOKEN: Preferences.Key = stringPreferencesKey("access_token") - val REFRESH_TOKEN: Preferences.Key = stringPreferencesKey("refresh_token") - } - - // Suspend save to avoid blocking thread - suspend fun saveToken(accessToken: String, refreshToken: String) { - dataStore.edit { prefs -> - prefs[Keys.ACCESS_TOKEN] = accessToken - prefs[Keys.REFRESH_TOKEN] = refreshToken - } - } - - // Synchronous getters backed by runBlocking for minimal surface change - fun getAccessToken(): String? = runBlocking { - dataStore.data.first()[Keys.ACCESS_TOKEN] - } - - fun getRefreshToken(): String? = runBlocking { - dataStore.data.first()[Keys.REFRESH_TOKEN] - } - - suspend fun clear() { - dataStore.edit { it.clear() } - } - - fun hasToken(): Boolean = !getAccessToken().isNullOrEmpty() && !getRefreshToken().isNullOrEmpty() -} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt deleted file mode 100644 index b79dea4..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.sampoom.android.feature.auth.data.mapper - -import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto -import com.sampoom.android.feature.auth.domain.model.User - -fun LoginResponseDto.toModel(): User = User(userId, userName, role, accessToken, refreshToken, expiresIn) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt deleted file mode 100644 index 41ee5bd..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.api - -import com.sampoom.android.core.network.ApiResponse -import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto -import retrofit2.http.Body -import retrofit2.http.POST - -interface AuthApi { - @POST("auth/login") - suspend fun login(@Body body: LoginRequestDto): ApiResponse - - @POST("auth/signup") - suspend fun signUp(@Body body: SignUpRequestDto): ApiResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpRequestDto.kt deleted file mode 100644 index ff24ca2..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpRequestDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.dto - -data class SignUpRequestDto( - val name: String, - val workspace: String, - val branch: String, - val position: String, - val email: String, - val password: String -) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt deleted file mode 100644 index 673d57a..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sampoom.android.feature.auth.data.repository - -import com.sampoom.android.feature.auth.data.local.preferences.AuthPreferences -import com.sampoom.android.feature.auth.data.mapper.toModel -import com.sampoom.android.feature.auth.data.remote.api.AuthApi -import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.repository.AuthRepository -import javax.inject.Inject - -class AuthRepositoryImpl @Inject constructor( - private val api: AuthApi, - private val preferences: AuthPreferences -) : AuthRepository { - override suspend fun signUp( - name: String, - workspace: String, - branch: String, - position: String, - email: String, - password: String - ): User { - api.signUp(SignUpRequestDto( - name = name, - workspace = workspace, - branch = branch, - position = position, - email = email, - password = password - )) - return signIn(email, password) - } - - override suspend fun signIn( - email: String, - password: String - ): User { - val dto = api.login(LoginRequestDto(email, password)) - preferences.saveToken(dto.data.accessToken, dto.data.refreshToken) - return dto.data.toModel() - } - - override suspend fun signOut() { preferences.clear() } - - override fun isSignedIn(): Boolean = preferences.hasToken() -} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt deleted file mode 100644 index d8479ae..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sampoom.android.feature.auth.domain.model - -data class User( - val id: Long, - val name: String, - val role: String, - val accessToken: String, - val refreshToken: String, - val expiresIn: Int -) diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt deleted file mode 100644 index 747e985..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.sampoom.android.feature.auth.domain.repository - -import com.sampoom.android.feature.auth.domain.model.User - -interface AuthRepository { - suspend fun signUp( - name: String, - workspace: String, - branch: String, - position: String, - email: String, - password: String - ): User - - suspend fun signIn(email: String, password: String): User - suspend fun signOut() - fun isSignedIn(): Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/AuthMappers.kt b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/AuthMappers.kt new file mode 100644 index 0000000..9108f35 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/AuthMappers.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.data.mapper + +import com.sampoom.android.feature.user.data.remote.dto.LoginResponseDto +import com.sampoom.android.feature.user.domain.model.User + +fun LoginResponseDto.toModel(): User = User(userId, userName, role, accessToken, refreshToken, expiresIn) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/AuthApi.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/AuthApi.kt new file mode 100644 index 0000000..21a588d --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/AuthApi.kt @@ -0,0 +1,37 @@ +package com.sampoom.android.feature.user.data.remote.api + +import com.sampoom.android.core.network.ApiResponse +import com.sampoom.android.core.network.ApiSuccessResponse +import com.sampoom.android.feature.user.data.remote.dto.LoginRequestDto +import com.sampoom.android.feature.user.data.remote.dto.SignUpRequestDto +import com.sampoom.android.feature.user.data.remote.dto.SignUpResponseDto +import com.sampoom.android.feature.user.data.remote.dto.LoginResponseDto +import com.sampoom.android.feature.user.data.remote.dto.RefreshRequestDto +import com.sampoom.android.feature.user.data.remote.dto.RefreshResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateResponseDto +import com.sampoom.android.feature.user.data.remote.dto.VerifyRequestDto +import com.sampoom.android.feature.user.data.remote.dto.VerifyResponseDto +import retrofit2.http.Body +import retrofit2.http.PATCH +import retrofit2.http.POST + +interface AuthApi { + @POST("auth/login") + suspend fun login(@Body body: LoginRequestDto): ApiResponse + + @POST("auth/logout") + suspend fun logout(): ApiSuccessResponse + + @POST("auth/refresh") + suspend fun refresh(@Body body: RefreshRequestDto): ApiResponse + + @POST("user/signup") + suspend fun signUp(@Body body: SignUpRequestDto): ApiResponse + + @POST("user/verify") + suspend fun verify(@Body body: VerifyRequestDto): ApiResponse + + @PATCH("user/update") + suspend fun update(@Body body: UpdateRequestDto): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginRequestDto.kt similarity index 57% rename from app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginRequestDto.kt rename to app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginRequestDto.kt index 136b9ac..95427ab 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginRequestDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginRequestDto.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.data.remote.dto +package com.sampoom.android.feature.user.data.remote.dto data class LoginRequestDto( val email: String, diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginResponseDto.kt similarity index 66% rename from app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginResponseDto.kt rename to app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginResponseDto.kt index 3d44982..e48ba2c 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginResponseDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/LoginResponseDto.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.data.remote.dto +package com.sampoom.android.feature.user.data.remote.dto data class LoginResponseDto( val userId: Long, @@ -6,5 +6,5 @@ data class LoginResponseDto( val role: String, val accessToken: String, val refreshToken: String, - val expiresIn: Int + val expiresIn: Long ) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshRequestDto.kt new file mode 100644 index 0000000..667e6c6 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class RefreshRequestDto( + val refreshToken: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshResponseDto.kt new file mode 100644 index 0000000..e9cfd8b --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/RefreshResponseDto.kt @@ -0,0 +1,7 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class RefreshResponseDto( + val accessToken: String, + val expiresIn: Long, + val refreshToken: String +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpRequestDto.kt new file mode 100644 index 0000000..2f611f8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpRequestDto.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class SignUpRequestDto( + val email: String, + val password: String, + val workspace: String, + val branch: String, + val userName: String, + val position: String +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpResponseDto.kt similarity index 64% rename from app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpResponseDto.kt rename to app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpResponseDto.kt index d683a69..f300491 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpResponseDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/SignUpResponseDto.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.data.remote.dto +package com.sampoom.android.feature.user.data.remote.dto data class SignUpResponseDto( val userId: Long, diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateRequestDto.kt new file mode 100644 index 0000000..24b7a03 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateRequestDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateRequestDto( + val userName: String, + val position: String, + val workspace: String, + val branch: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateResponseDto.kt new file mode 100644 index 0000000..b5b2cd5 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateResponseDto.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateResponseDto( + val userId: Long, + val email: String, + val userName: String, + val position: String, + val workspace: String, + val branch: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyRequestDto.kt new file mode 100644 index 0000000..0ee23f1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyRequestDto.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class VerifyRequestDto( + val email: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyResponseDto.kt new file mode 100644 index 0000000..9d061f6 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/VerifyResponseDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class VerifyResponseDto( + val userId: Long, + val email: String, + val userName: String, + val role: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..620b6e8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,73 @@ +package com.sampoom.android.feature.user.data.repository + +import com.sampoom.android.core.datastore.AuthPreferences +import com.sampoom.android.feature.user.data.mapper.toModel +import com.sampoom.android.feature.user.data.remote.api.AuthApi +import com.sampoom.android.feature.user.data.remote.dto.LoginRequestDto +import com.sampoom.android.feature.user.data.remote.dto.RefreshRequestDto +import com.sampoom.android.feature.user.data.remote.dto.SignUpRequestDto +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.AuthRepository +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor( + private val api: AuthApi, + private val preferences: AuthPreferences +) : AuthRepository { + override suspend fun signUp( + email: String, + password: String, + workspace: String, + branch: String, + userName: String, + position: String + ): User { + api.signUp(SignUpRequestDto( + email = email, + password = password, + workspace = workspace, + branch = branch, + userName = userName, + position = position + )) + return signIn(email, password) + } + + override suspend fun signIn( + email: String, + password: String + ): User { + val dto = api.login(LoginRequestDto(email, password)) + val user = dto.data.toModel() + preferences.saveUser(user) + return user + } + + override suspend fun signOut() { + runCatching { api.logout() } + .onFailure { throw Exception("Failed logout") } + preferences.clear() + } + + override suspend fun refreshToken(): Result { + return runCatching { + val refreshToken = preferences.getRefreshToken() ?: throw Exception("No refresh token available") + val response = api.refresh(RefreshRequestDto(refreshToken)) + val existingUser = preferences.getStoredUser() ?: throw Exception("No user information available") + + val updatedUser = existingUser.copy( + accessToken = response.data.accessToken, + refreshToken = response.data.refreshToken, + expiresIn = response.data.expiresIn + ) + preferences.saveUser(updatedUser) + updatedUser + } + } + + override suspend fun clearTokens() { + preferences.clear() + } + + override suspend fun isSignedIn(): Boolean = preferences.hasToken() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/di/AuthModules.kt b/app/src/main/java/com/sampoom/android/feature/user/di/AuthModules.kt similarity index 71% rename from app/src/main/java/com/sampoom/android/feature/auth/di/AuthModules.kt rename to app/src/main/java/com/sampoom/android/feature/user/di/AuthModules.kt index 87b7f09..0be2f12 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/di/AuthModules.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/di/AuthModules.kt @@ -1,8 +1,8 @@ -package com.sampoom.android.feature.auth.di +package com.sampoom.android.feature.user.di -import com.sampoom.android.feature.auth.data.remote.api.AuthApi -import com.sampoom.android.feature.auth.data.repository.AuthRepositoryImpl -import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.data.remote.api.AuthApi +import com.sampoom.android.feature.user.data.repository.AuthRepositoryImpl +import com.sampoom.android.feature.user.domain.repository.AuthRepository import dagger.Binds import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/AuthValidator.kt similarity index 98% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt rename to app/src/main/java/com/sampoom/android/feature/user/domain/AuthValidator.kt index b7c4516..5b3bc1e 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/AuthValidator.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.domain +package com.sampoom.android.feature.user.domain import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt new file mode 100644 index 0000000..84be2f9 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.model + +data class User( + val userId: Long, + val userName: String, + val role: String, + val accessToken: String, + val refreshToken: String, + val expiresIn: Long +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/AuthRepository.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/AuthRepository.kt new file mode 100644 index 0000000..fd8b5f9 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/AuthRepository.kt @@ -0,0 +1,20 @@ +package com.sampoom.android.feature.user.domain.repository + +import com.sampoom.android.feature.user.domain.model.User + +interface AuthRepository { + suspend fun signUp( + email: String, + password: String, + workspace: String, + branch: String, + userName: String, + position: String + ): User + + suspend fun signIn(email: String, password: String): User + suspend fun signOut() + suspend fun refreshToken(): Result + suspend fun clearTokens() + suspend fun isSignedIn(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/CheckLoginStateUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/CheckLoginStateUseCase.kt new file mode 100644 index 0000000..f63ad59 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/CheckLoginStateUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.repository.AuthRepository +import javax.inject.Inject + +class CheckLoginStateUseCase @Inject constructor( + private val repository: AuthRepository +) { + suspend operator fun invoke(): Boolean = repository.isSignedIn() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/ClearTokensUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/ClearTokensUseCase.kt new file mode 100644 index 0000000..c46ace8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/ClearTokensUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.repository.AuthRepository +import javax.inject.Inject + +class ClearTokensUseCase @Inject constructor( + private val repository: AuthRepository +) { + suspend operator fun invoke() = repository.clearTokens() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/LoginUseCase.kt similarity index 57% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt rename to app/src/main/java/com/sampoom/android/feature/user/domain/usecase/LoginUseCase.kt index d31282c..1f01e3d 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/LoginUseCase.kt @@ -1,7 +1,7 @@ -package com.sampoom.android.feature.auth.domain.usecase +package com.sampoom.android.feature.user.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.AuthRepository import javax.inject.Inject class LoginUseCase @Inject constructor( diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignOutUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignOutUseCase.kt new file mode 100644 index 0000000..fb896b4 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignOutUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.repository.AuthRepository +import javax.inject.Inject + +class SignOutUseCase @Inject constructor( + private val repository: AuthRepository +) { + suspend operator fun invoke() = repository.signOut() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignUpUseCase.kt similarity index 50% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt rename to app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignUpUseCase.kt index 9fa4ddd..f64e7f6 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/SignUpUseCase.kt @@ -1,25 +1,25 @@ -package com.sampoom.android.feature.auth.domain.usecase +package com.sampoom.android.feature.user.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.AuthRepository import javax.inject.Inject class SignUpUseCase @Inject constructor( private val repository: AuthRepository ) { suspend operator fun invoke( - name: String, + email: String, + password: String, workspace: String, branch: String, - position: String, - email: String, - password: String + userName: String, + position: String ): User = repository.signUp( - name = name, + email = email, + password = password, workspace = workspace, branch = branch, - position = position, - email = email, - password = password + userName = userName, + position = position ) } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/AuthViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/AuthViewModel.kt new file mode 100644 index 0000000..d81a398 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/AuthViewModel.kt @@ -0,0 +1,54 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.feature.user.domain.usecase.CheckLoginStateUseCase +import com.sampoom.android.feature.user.domain.usecase.ClearTokensUseCase +import com.sampoom.android.feature.user.domain.usecase.SignOutUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AuthViewModel @Inject constructor( + private val checkLoginStateUseCase: CheckLoginStateUseCase, + private val signOutUseCase: SignOutUseCase, + private val clearTokensUseCase: ClearTokensUseCase +) : ViewModel() { + private val _isLoggedIn = MutableStateFlow(false) + val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() + + private val _logoutEvent = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1 + ) + val logoutEvent: SharedFlow = _logoutEvent.asSharedFlow() + + init { + viewModelScope.launch { + _isLoggedIn.value = checkLoginStateUseCase() + } + } + + fun updateLoginState() = viewModelScope.launch { + _isLoggedIn.value = checkLoginStateUseCase() + } + + fun signOut() = viewModelScope.launch { + signOutUseCase() + _isLoggedIn.value = false + _logoutEvent.emit(Unit) + } + + fun handleTokenExpired() = viewModelScope.launch { + clearTokensUseCase() + _isLoggedIn.value = false + _logoutEvent.emit(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginScreen.kt similarity index 98% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/LoginScreen.kt index a894c8d..c2e758b 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -39,7 +39,6 @@ import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.component.ShowErrorSnackBar import com.sampoom.android.core.ui.component.rememberCommonSnackBarHostState import com.sampoom.android.core.ui.component.TopSnackBarHost -import com.sampoom.android.core.ui.theme.backgroundColor @Composable fun LoginScreen( diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiEvent.kt similarity index 82% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiEvent.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiEvent.kt index ea2af9a..7b68d9d 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiEvent.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui sealed interface LoginUiEvent { data class EmailChanged(val email: String) : LoginUiEvent diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiState.kt similarity index 91% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiState.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiState.kt index 2546cab..fb34a74 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginUiState.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui data class LoginUiState( val email: String = "", diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginViewModel.kt similarity index 92% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/LoginViewModel.kt index e2f404a..0c0f1a9 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/LoginViewModel.kt @@ -1,12 +1,12 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui import android.app.Application import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult -import com.sampoom.android.feature.auth.domain.usecase.LoginUseCase +import com.sampoom.android.feature.user.domain.AuthValidator +import com.sampoom.android.feature.user.domain.ValidationResult +import com.sampoom.android.feature.user.domain.usecase.LoginUseCase import com.sampoom.android.core.network.serverMessageOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpScreen.kt similarity index 97% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/SignUpScreen.kt index b252289..d118ccd 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpScreen.kt @@ -1,7 +1,6 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -19,7 +18,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -42,7 +40,6 @@ import com.sampoom.android.core.ui.component.CommonTextField import com.sampoom.android.core.ui.component.ShowErrorSnackBar import com.sampoom.android.core.ui.component.rememberCommonSnackBarHostState import com.sampoom.android.core.ui.component.TopSnackBarHost -import com.sampoom.android.core.ui.theme.backgroundColor @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiEvent.kt similarity index 91% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiEvent.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiEvent.kt index eb8e77b..2f355e0 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiEvent.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui sealed interface SignUpUiEvent { data class NameChanged(val name: String) : SignUpUiEvent diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiState.kt similarity index 96% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiState.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiState.kt index 524c1fe..754be8e 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpUiState.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui data class SignUpUiState( val name: String = "", diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpViewModel.kt similarity index 93% rename from app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt rename to app/src/main/java/com/sampoom/android/feature/user/ui/SignUpViewModel.kt index 73d29b6..15a5704 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/SignUpViewModel.kt @@ -1,12 +1,12 @@ -package com.sampoom.android.feature.auth.ui +package com.sampoom.android.feature.user.ui import android.app.Application import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult -import com.sampoom.android.feature.auth.domain.usecase.SignUpUseCase +import com.sampoom.android.feature.user.domain.AuthValidator +import com.sampoom.android.feature.user.domain.ValidationResult +import com.sampoom.android.feature.user.domain.usecase.SignUpUseCase import com.sampoom.android.core.network.serverMessageOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -137,12 +137,12 @@ class SignUpViewModel @Inject constructor( _state.update { it.copy(loading = true, error = null) } runCatching { singUp( - name = s.name, + email = s.email, + password = s.password, workspace = s.workspace, branch = s.branch, - position = s.position, - email = s.email, - password = s.password + userName = s.name, + position = s.position ) } .onSuccess { _state.update { it.copy(loading = false, success = true) } }