Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0960581
[feat]: 네트워크 보안 설정 파일 추가 및 gitignore에 추가 (#26)
rbqks529 Oct 2, 2025
4a531e1
[feat]: 이메일 인증 Dto 추가 (#26)
rbqks529 Oct 2, 2025
de08604
[feat]: DataSource에 이메일 인증 함수 추가 (#26)
rbqks529 Oct 2, 2025
2513f64
[feat]: 이메일 인증 함수 Repository에 구현 및 화면 연결 (#26)
rbqks529 Oct 2, 2025
73014ed
[feat]: 로딩 인디케이터 추가 (#26)
rbqks529 Oct 2, 2025
3a35802
[feat]: 인증코드 입력 박스 UI 재수정 (#26)
rbqks529 Oct 2, 2025
2fd2c9f
[feat]: Route에서 이메일을 다음 화면으로 넘기도록 수정 (#26)
rbqks529 Oct 2, 2025
ccb686f
[feat]: 이메일 인증 RequestDto 수정 (#26)
rbqks529 Oct 2, 2025
ff0eb96
[feat]: 이메일 인증 로직 DataSource에 구현 (#26)
rbqks529 Oct 2, 2025
66c62ba
[feat]: 이메일 인증 코드 입력 함수 Screen에 연결 (#26)
rbqks529 Oct 2, 2025
0a16530
[feat]: 인증 코드 입력 로직 수정 (#26)
rbqks529 Oct 2, 2025
6ed7154
[feat]: 인증 코드 입력 로직 수정 (#26)
rbqks529 Oct 2, 2025
07575cd
[feat]: 회원가입 Dto 추가 (#26)
rbqks529 Oct 3, 2025
ac296f0
[feat]: 비밀번호 입력 화면 조건 수정 (#26)
rbqks529 Oct 3, 2025
401ec81
[feat]: DataSource, Repository에 함수 추가 (#26)
rbqks529 Oct 3, 2025
f18c0c3
[feat]: 루트 파라미터 수정 (#26)
rbqks529 Oct 3, 2025
0b35a22
[feat]: 회원가입 viewModel 추가 (#26)
rbqks529 Oct 3, 2025
9065a1b
[feat]: nickName 입력 화면에 회원가입 로직 연결 (#26)
rbqks529 Oct 3, 2025
cc0320c
[feat]: 기존 로그인 로직을 api 스펙에 맞게 수정 (#26)
rbqks529 Oct 3, 2025
8121194
[feat]: 회원가입 성공시 자동 로그인 (#26)
rbqks529 Oct 3, 2025
51d143a
[feat]: 로그인 화면에 로그인 로직 연결 (#26)
rbqks529 Oct 3, 2025
ff9057a
[feat]: 비밀번호 찾기 Dto 추가 (#26)
rbqks529 Oct 3, 2025
4f8acbd
[feat]: 임시 비밀번호 발급 함수 구현 (#26)
rbqks529 Oct 3, 2025
38c1a9d
[feat]: 임시 비밀번호 발급 화면 viewModel 구현 (#26)
rbqks529 Oct 3, 2025
505bea0
[feat]: 임시 비밀번호 발급 화면 로직과 연결 (#26)
rbqks529 Oct 3, 2025
9be520c
[refactor]: Auth 로직을 다른 파일로 분리 (#26)
rbqks529 Oct 5, 2025
784ac98
[refactor]: 매니페스트 파일 수정 (#26)
rbqks529 Oct 5, 2025
148128f
feat: develop 기준 코드 최신화 (#26)
rbqks529 Oct 5, 2025
691d649
[refactor]: 로그인 화면 로직 수정 (#26)
rbqks529 Oct 5, 2025
76de901
[refactor]: path 수정 (#26)
rbqks529 Oct 5, 2025
8571dde
[feat]: DataStore에 토큰이 있을 경우 자동 로그인이 되도록 구현 (#26)
rbqks529 Oct 5, 2025
f241086
[refactor]: 기존 코드의 중복 제거 (#26)
rbqks529 Oct 5, 2025
8623ced
[refactor]: 리프래시 토큰 재발급 로직 및 토큰 만료시 로그인 화면으로 네비게이션 로직 구현 (#26)
rbqks529 Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ xcuserdata
!src/**/build/
local.properties
.idea
**/network_security_config.xml
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

network_security_config.xml은 버전 관리 대상입니다.

앱의 네트워크 보안 정책을 정의하는 파일을 전역으로 무시하면, 환경마다 다른 설정이 조용히 적용되고 보안 검토도 불가능해집니다. 규정·릴리스 안정성 모두 큰 리스크이니 해당 ignore 항목을 제거하거나, 필요한 경우 별도의 샘플 파일/로컬 경로만 선택적으로 무시해 주세요.

🤖 Prompt for AI Agents
In .gitignore around line 9, the global ignore entry for
**/network_security_config.xml causes the app's network security policy file to
be excluded from version control; remove this ignore line so
network_security_config.xml files are tracked, or replace it with a scoped
ignore that only excludes a sample/local path (e.g.,
/app/src/main/res/xml/network_security_config.sample.xml or a developer-specific
path) and add a tracked canonical network_security_config.xml (or a template
checked into repo) so environments and security reviews remain consistent.

.DS_Store
captures
.externalNativeBuild
Expand Down
17 changes: 15 additions & 2 deletions composeApp/src/commonMain/kotlin/org/whosin/client/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package org.whosin.client
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBarsPadding
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.navigation.compose.rememberNavController
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.whosin.client.core.auth.TokenExpiredManager
import org.whosin.client.core.navigation.Route
import org.whosin.client.core.navigation.WhosInNavGraph
import org.whosin.client.presentation.dummy.DummyScreen
import org.whosin.client.presentation.dummy.TokenTestScreen
import ui.theme.WhosInTheme


Expand All @@ -17,6 +20,16 @@ import ui.theme.WhosInTheme
fun App() {
WhosInTheme {
val navController = rememberNavController()
val isTokenExpired by TokenExpiredManager.isTokenExpired.collectAsState()

LaunchedEffect(isTokenExpired) {
if (isTokenExpired) {
navController.navigate(Route.Login) {
popUpTo(0) { inclusive = true }
}
TokenExpiredManager.reset()
}
}

WhosInNavGraph(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.whosin.client.core.auth

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

object TokenExpiredManager {
private val _isTokenExpired = MutableStateFlow(false)
val isTokenExpired: StateFlow<Boolean> = _isTokenExpired.asStateFlow()

fun setTokenExpired() {
_isTokenExpired.value = true
}

fun reset() {
_isTokenExpired.value = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ sealed interface Route {
data object Signup: Route

@Serializable
data object EmailVerification: Route
data class EmailVerification(val email: String): Route

@Serializable
data object PasswordInput: Route
data class PasswordInput(val email: String): Route

@Serializable
data object NicknameInput: Route
data class NicknameInput(val email: String, val password: String): Route

@Serializable
data class ClubCodeInput(val returnToMyPage: Boolean = false): Route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ fun WhosInNavGraph(
navController.navigate(Route.Login) {
popUpTo(Route.Splash) { inclusive = true }
}
},
onNavigateToHome = {
navController.navigate(Route.Home) {
popUpTo(Route.AuthGraph) { inclusive = true }
}
}
)
}
Expand Down Expand Up @@ -74,35 +79,43 @@ fun WhosInNavGraph(
modifier = modifier,
onNavigateBack = { navController.navigateUp() },
onNavigateToEmailVerification = { email ->
navController.navigate(Route.EmailVerification)
navController.navigate(Route.EmailVerification(email))
}
)
}

composable<Route.EmailVerification> { backStackEntry ->

val emailVerificationRoute = backStackEntry.toRoute<Route.EmailVerification>()

EmailVerificationScreen(
modifier = modifier,
email = emailVerificationRoute.email,
onNavigateBack = { navController.navigateUp() },
onVerificationComplete = {
navController.navigate(Route.PasswordInput)
navController.navigate(Route.PasswordInput(emailVerificationRoute.email))
}
)
}

composable<Route.PasswordInput> {

composable<Route.PasswordInput> { backStackEntry ->
val passwordInputRoute = backStackEntry.toRoute<Route.PasswordInput>()

PasswordInputScreen(
modifier = modifier,
onNavigateBack = { navController.navigateUp() },
onPasswordComplete = { password, confirmPassword ->
navController.navigate(Route.NicknameInput)
navController.navigate(Route.NicknameInput(passwordInputRoute.email, password))
}
)
}

composable<Route.NicknameInput> {

composable<Route.NicknameInput> { backStackEntry ->
val nicknameInputRoute = backStackEntry.toRoute<Route.NicknameInput>()

NicknameInputScreen(
modifier = modifier,
email = nicknameInputRoute.email,
password = nicknameInputRoute.password,
onNavigateBack = { navController.navigateUp() },
onNavigateToClubCode = {
navController.navigate(Route.ClubCodeInput(returnToMyPage = false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import io.ktor.http.encodedPath
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import org.whosin.client.BuildKonfig
import org.whosin.client.core.auth.TokenExpiredManager
import org.whosin.client.core.datastore.TokenManager
import org.whosin.client.data.dto.request.ReissueTokenRequestDto
import org.whosin.client.data.dto.response.TokenDto
import org.whosin.client.data.dto.response.ReissueTokenResponseDto

object HttpClientFactory {
val BASE_URL = BuildKonfig.BASE_URL
Expand All @@ -45,10 +46,11 @@ object HttpClientFactory {
socketTimeoutMillis = 20_000L
requestTimeoutMillis = 20_000L
}
install(Auth){
install(Auth) {
bearer {
loadTokens {
val accessToken = tokenManager.getAccessToken() ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
val accessToken = tokenManager.getAccessToken()
?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
Comment on lines +52 to 55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

하드코딩된 JWT 제거 필요

Line 52~55에서 액세스 토큰과 리프레시 토큰이 하드코딩된 문자열로 대체되고 있습니다. 인증이 되지 않은 상태에서도 이 더미 토큰이 모든 요청 헤더에 실리기 때문에, 실서버로 빌드가 나가면 불특정 사용자에게 동일한 권한이 노출되는 치명적인 보안 사고로 이어질 수 있습니다. 또한 정적 분석(gitleaks)도 JWT 유출로 경고하고 있습니다. 토큰이 없으면 null을 반환해 Bearer 헤더를 보내지 않도록 고쳐 주세요.

-                    loadTokens {
-                        val accessToken = tokenManager.getAccessToken()
-                            ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
-                        val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
-                        BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
-                    }
+                    loadTokens {
+                        val accessToken = tokenManager.getAccessToken()
+                        val refreshToken = tokenManager.getRefreshToken()
+                        if (accessToken != null && refreshToken != null) {
+                            BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
+                        } else {
+                            null
+                        }
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val accessToken = tokenManager.getAccessToken()
?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
loadTokens {
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
if (accessToken != null && refreshToken != null) {
BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
} else {
null
}
}
🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 53-53: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt
around lines 52 to 55, remove the hardcoded JWT and "no_token" fallback;
instead, do not fabricate tokens — if tokenManager.getAccessToken() returns null
then return null (or omit constructing BearerTokens) so no Authorization header
is sent, and only construct BearerTokens when a real access token exists
(refreshToken may be null). Ensure no static token strings remain and update any
callers to handle a nullable BearerTokens result.

}
Expand All @@ -75,7 +77,7 @@ object HttpClientFactory {
"auth/login",
"auth/email",
"auth/email/validation",
"member/reissue" // 토큰 재발급 요청 자체에는 만료된 액세스 토큰을 보내면 안 됨
"auth/reissue" // 토큰 재발급 요청
)

val isNoAuthPath = pathWithNoAuth.any { noAuthPath ->
Expand All @@ -88,26 +90,42 @@ object HttpClientFactory {
}
}
refreshTokens {
val rt = tokenManager.getRefreshToken() ?: "no_token"
val response = client.post("member/reissue"){
setBody {
ReissueTokenRequestDto(
refreshToken = rt
try {
val rt = tokenManager.getRefreshToken()
if (rt.isNullOrEmpty()) {
tokenManager.clearToken()
TokenExpiredManager.setTokenExpired()
return@refreshTokens null
}

val response = client.post("auth/reissue") {
setBody(ReissueTokenRequestDto(refreshToken = rt))
markAsRefreshTokenRequest()
}.body<ReissueTokenResponseDto>()

if (response.success && response.data != null) {
tokenManager.saveTokens(
accessToken = response.data.accessToken,
refreshToken = response.data.refreshToken
)
BearerTokens(
accessToken = response.data.accessToken,
refreshToken = response.data.refreshToken
)
} else {
tokenManager.clearToken()
TokenExpiredManager.setTokenExpired()
null
}
markAsRefreshTokenRequest()
}.body<TokenDto>()
tokenManager.saveTokens(
accessToken = response.accessToken,
refreshToken = response.refreshToken
)
val accessToken = response.accessToken
val refreshToken = response.refreshToken
BearerTokens(accessToken,refreshToken)
} catch (e: Exception) {
tokenManager.clearToken()
TokenExpiredManager.setTokenExpired()
null
}
}
}
}
install(Logging){
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
println(message)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.whosin.client.data.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class EmailValidationRequestDto(
@SerialName("email")
val email: String,
@SerialName("authCode")
val authCode: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.whosin.client.data.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class EmailVerificationRequestDto(
@SerialName("email")
val email: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.whosin.client.data.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class FindPasswordRequestDto(
@SerialName("email")
val email: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.whosin.client.data.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SignupRequestDto(
@SerialName("email")
val email: String,
@SerialName("password")
val password: String,
@SerialName("nickName")
val nickName: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.whosin.client.data.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class EmailVerificationResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: String? = null,
@SerialName("timestamp")
val timestamp: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.whosin.client.data.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class FindPasswordResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: String? = null,
@SerialName("timestamp")
val timestamp: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import kotlinx.serialization.Serializable
data class LoginResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("code")
val code: Int,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: TokenDto
val data: TokenDto? = null,
@SerialName("timestamp")
val timestamp: String? = null
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.whosin.client.data.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ReissueTokenResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: TokenDto? = null,
@SerialName("timestamp")
val timestamp: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.whosin.client.data.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SignupResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: String? = null,
@SerialName("timestamp")
val timestamp: String? = null
)
Loading