Skip to content

Commit d43514b

Browse files
authored
Merge pull request #11 from YAPP-Github/feature/NDGL-18
[NDGL-18] 로그인 및 유저 생성 API 연동
2 parents d65d89d + fdef676 commit d43514b

File tree

11 files changed

+223
-8
lines changed

11 files changed

+223
-8
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<uses-permission android:name="android.permission.INTERNET" />
6+
57
<application
68
android:name=".NDGLApplication"
79
android:allowBackup="true"
@@ -11,7 +13,8 @@
1113
android:label="@string/app_name"
1214
android:roundIcon="@mipmap/ic_launcher_round"
1315
android:supportsRtl="true"
14-
android:theme="@style/Theme.NDGL">
16+
android:theme="@style/Theme.NDGL"
17+
android:usesCleartextTraffic="true">
1518
<activity
1619
android:name=".MainActivity"
1720
android:exported="true"

build-logic/src/main/kotlin/NDGLDataPlugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class NDGLDataPlugin : Plugin<Project> {
2424
}
2525

2626
dependencies {
27+
"implementation"(project(":core:util"))
2728
"implementation"(libs.findLibrary("retrofit").get())
2829
"implementation"(libs.findLibrary("retrofit-kotlinx-serialization-json").get())
2930
"implementation"(libs.findLibrary("kotlinx-serialization-json").get())

data/auth/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@ plugins {
44

55
android {
66
namespace = "com.yapp.ndgl.data.auth"
7+
8+
buildFeatures {
9+
buildConfig = true
10+
}
11+
12+
defaultConfig {
13+
buildConfigField("String", "VERSION_NAME", "\"${Configuration.VERSION_NAME}\"")
14+
}
715
}
816

917
dependencies {
18+
implementation(project(":data:core"))
19+
implementation(platform(libs.firebase.bom))
20+
implementation(libs.firebase.messaging)
1021
implementation(libs.androidx.datastore)
1122
}

data/auth/src/main/java/com/yapp/ndgl/data/auth/local/LocalAuthDataSource.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
44
import androidx.datastore.preferences.core.Preferences
55
import androidx.datastore.preferences.core.edit
66
import androidx.datastore.preferences.core.stringPreferencesKey
7+
import com.yapp.ndgl.data.auth.local.security.CryptoManager
78
import com.yapp.ndgl.data.auth.local.util.handleException
89
import kotlinx.coroutines.flow.Flow
910
import kotlinx.coroutines.flow.first
@@ -14,17 +15,18 @@ import javax.inject.Singleton
1415
@Singleton
1516
class LocalAuthDataSource @Inject constructor(
1617
private val dataStore: DataStore<Preferences>,
18+
private val cryptoManager: CryptoManager,
1719
) {
1820
private val accessToken: Flow<String> = dataStore.data
1921
.handleException()
2022
.map { preferences ->
21-
preferences[ACCESS_TOKEN_KEY] ?: ""
23+
cryptoManager.decrypt(preferences[ACCESS_TOKEN_KEY] ?: "")
2224
}
2325

2426
private val uuid: Flow<String> = dataStore.data
2527
.handleException()
2628
.map { preferences ->
27-
preferences[UUID_KEY] ?: ""
29+
cryptoManager.decrypt(preferences[UUID_KEY] ?: "")
2830
}
2931

3032
suspend fun getAccessToken(): String = accessToken.first()
@@ -33,13 +35,13 @@ class LocalAuthDataSource @Inject constructor(
3335

3436
suspend fun setAccessToken(token: String) {
3537
dataStore.edit { preferences ->
36-
preferences[ACCESS_TOKEN_KEY] = token
38+
preferences[ACCESS_TOKEN_KEY] = cryptoManager.encrypt(token)
3739
}
3840
}
3941

4042
suspend fun setUuid(uuid: String) {
4143
dataStore.edit { preferences ->
42-
preferences[UUID_KEY] = uuid
44+
preferences[UUID_KEY] = cryptoManager.encrypt(uuid)
4345
}
4446
}
4547

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.yapp.ndgl.data.auth.local.security
2+
3+
import android.security.keystore.KeyGenParameterSpec
4+
import android.security.keystore.KeyProperties
5+
import android.util.Base64
6+
import timber.log.Timber
7+
import java.security.KeyStore
8+
import javax.crypto.Cipher
9+
import javax.crypto.KeyGenerator
10+
import javax.crypto.SecretKey
11+
import javax.crypto.spec.GCMParameterSpec
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class CryptoManager @Inject constructor() {
17+
private val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
18+
load(null)
19+
}
20+
21+
private fun getKey(): SecretKey {
22+
return keyStore.getKey(KEY_ALIAS, null) as? SecretKey ?: createKey()
23+
}
24+
25+
private fun createKey(): SecretKey =
26+
KeyGenerator.getInstance(
27+
KeyProperties.KEY_ALGORITHM_AES,
28+
KEYSTORE_PROVIDER,
29+
).apply {
30+
init(
31+
KeyGenParameterSpec.Builder(
32+
KEY_ALIAS,
33+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
34+
)
35+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
36+
.setEncryptionPaddings(PADDING)
37+
.setUserAuthenticationRequired(false)
38+
.build(),
39+
)
40+
}.generateKey()
41+
42+
fun encrypt(plainText: String): String {
43+
check(plainText.isNotEmpty()) { "Plain text is empty" }
44+
45+
val cipher = Cipher.getInstance(TRANSFORMATION)
46+
cipher.init(Cipher.ENCRYPT_MODE, getKey())
47+
48+
val encryptedBytes = cipher.doFinal(plainText.toByteArray())
49+
val combined = cipher.iv + encryptedBytes
50+
51+
return Base64.encodeToString(combined, Base64.NO_WRAP)
52+
}
53+
54+
fun decrypt(encryptedText: String): String {
55+
try {
56+
check(encryptedText.isNotEmpty()) { "Encrypted text is empty" }
57+
58+
val combined = Base64.decode(encryptedText, Base64.NO_WRAP)
59+
val iv = combined.copyOfRange(0, IV_SIZE)
60+
val encryptedBytes = combined.copyOfRange(IV_SIZE, combined.size)
61+
62+
val cipher = Cipher.getInstance(TRANSFORMATION)
63+
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
64+
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
65+
66+
val decryptedBytes = cipher.doFinal(encryptedBytes)
67+
return String(decryptedBytes)
68+
} catch (e: Exception) {
69+
Timber.e(e, "Failed to decrypt")
70+
return ""
71+
}
72+
}
73+
74+
companion object {
75+
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
76+
private const val KEY_ALIAS = "ndgl_encryption_key"
77+
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
78+
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
79+
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
80+
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
81+
private const val IV_SIZE = 12
82+
private const val GCM_TAG_LENGTH = 128
83+
}
84+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.yapp.ndgl.data.auth.repository
2+
3+
import com.google.android.gms.tasks.Tasks
4+
import com.google.firebase.messaging.FirebaseMessaging
5+
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.auth.BuildConfig
7+
import com.yapp.ndgl.data.auth.api.AuthApi
8+
import com.yapp.ndgl.data.auth.local.LocalAuthDataSource
9+
import com.yapp.ndgl.data.auth.model.AuthResponse
10+
import com.yapp.ndgl.data.auth.model.CreateUserRequest
11+
import com.yapp.ndgl.data.auth.model.LoginRequest
12+
import com.yapp.ndgl.data.auth.util.DeviceInfoUtil
13+
import com.yapp.ndgl.data.core.model.getData
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.withContext
16+
import javax.inject.Inject
17+
import javax.inject.Singleton
18+
19+
@Singleton
20+
class AuthRepository @Inject constructor(
21+
private val api: AuthApi,
22+
private val localAuthDataSource: LocalAuthDataSource,
23+
) {
24+
suspend fun initSession() {
25+
val uuid = localAuthDataSource.getUuid()
26+
val response = if (uuid.isNotEmpty()) {
27+
suspendRunCatching {
28+
login(uuid)
29+
}.getOrElse {
30+
localAuthDataSource.clearSession()
31+
createUser()
32+
}
33+
} else {
34+
createUser()
35+
}
36+
37+
localAuthDataSource.setAccessToken(response.accessToken)
38+
localAuthDataSource.setUuid(response.uuid)
39+
}
40+
41+
private suspend fun createUser(): AuthResponse {
42+
return api.createUser(
43+
CreateUserRequest(
44+
fcmToken = getFCMToken(),
45+
deviceModel = DeviceInfoUtil.deviceModel,
46+
deviceOs = DeviceInfoUtil.DEVICE_OS,
47+
deviceOsVersion = DeviceInfoUtil.deviceOsVersion,
48+
appVersion = BuildConfig.VERSION_NAME,
49+
),
50+
).getData()
51+
}
52+
53+
private suspend fun login(uuid: String): AuthResponse {
54+
return api.login(LoginRequest(uuid)).getData()
55+
}
56+
57+
private suspend fun getFCMToken(): String = withContext(Dispatchers.IO) {
58+
try {
59+
Tasks.await(FirebaseMessaging.getInstance().token)
60+
} catch (e: Exception) {
61+
throw IllegalStateException("Failed to get FCM token", e)
62+
}
63+
}
64+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.yapp.ndgl.data.auth.util
2+
3+
import android.os.Build
4+
5+
object DeviceInfoUtil {
6+
const val DEVICE_OS: String = "Android"
7+
val deviceModel: String = Build.MODEL
8+
val deviceOsVersion: String = Build.VERSION.RELEASE
9+
}

feature/home/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ android {
88

99
dependencies {
1010
implementation(project(":data:travel"))
11+
implementation(project(":data:auth"))
1112
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.yapp.ndgl.feature.home
2+
3+
import com.yapp.ndgl.core.base.UiIntent
4+
import com.yapp.ndgl.core.base.UiSideEffect
5+
import com.yapp.ndgl.core.base.UiState
6+
7+
data class HomeState(
8+
val isLoading: Boolean = false,
9+
) : UiState
10+
11+
sealed interface HomeIntent : UiIntent
12+
13+
sealed interface HomeSideEffect : UiSideEffect
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
package com.yapp.ndgl.feature.home
22

3-
import androidx.lifecycle.ViewModel
3+
import androidx.lifecycle.viewModelScope
4+
import com.yapp.ndgl.core.base.BaseViewModel
5+
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.auth.repository.AuthRepository
47
import dagger.hilt.android.lifecycle.HiltViewModel
8+
import kotlinx.coroutines.launch
9+
import timber.log.Timber
510
import javax.inject.Inject
611

712
@HiltViewModel
8-
class HomeViewModel @Inject constructor() : ViewModel()
13+
class HomeViewModel @Inject constructor(
14+
private val authRepository: AuthRepository,
15+
) : BaseViewModel<HomeState, HomeIntent, HomeSideEffect>(
16+
initialState = HomeState(),
17+
) {
18+
init {
19+
initSession()
20+
}
21+
22+
override suspend fun handleIntent(intent: HomeIntent) {
23+
TODO("Not yet implemented")
24+
}
25+
26+
private fun initSession() = viewModelScope.launch {
27+
suspendRunCatching {
28+
authRepository.initSession()
29+
}.onSuccess {
30+
Timber.d("initSession() 성공")
31+
}.onFailure {
32+
Timber.e("initSession() 실패")
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)