Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:usesCleartextTraffic="true">
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

❓ Verification inconclusive

평문 트래픽 허용은 보안 위험이 있습니다.

android:usesCleartextTraffic="true" 설정은 암호화되지 않은 HTTP 통신을 허용합니다. iOS 측 Info.plist와 동일하게, 이는 중간자 공격에 노출될 수 있는 보안 위험이 있습니다.

개발 단계라면 임시로 사용할 수 있지만, 프로덕션 릴리스 전에는 반드시:

  1. 백엔드 API를 HTTPS로 전환
  2. 이 설정을 false로 변경하거나 제거

해야 합니다. Android 9(API 28) 이상에서는 기본적으로 cleartext 트래픽이 차단되므로, 보안을 위해 HTTPS 사용을 권장합니다.


AndroidManifest.xml(14행) cleartext 트래픽 허용 설정 제거
android:usesCleartextTraffic="true"는 중간자 공격 등에 취약하므로 개발용 임시 설정을 제외하고 프로덕션 전에는 false로 변경하거나 제거하고, 백엔드 API를 HTTPS로 전환해야 합니다.

🤖 Prompt for AI Agents
In composeApp/src/androidMain/AndroidManifest.xml around line 14, remove or set
android:usesCleartextTraffic="false" instead of true so the app does not allow
cleartext (HTTP) traffic in production; ensure backend endpoints use HTTPS and,
if necessary for development, gate any temporary cleartext allowance behind a
debug-only manifest or network security config not shipped to production.

<activity
android:exported="true"
android:name=".MainActivity">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.whosin.client.presentation.component

import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable

@Composable
actual fun CommonBackHandler(enabled: Boolean, onBack: () -> Unit) {
BackHandler(enabled = enabled, onBack = onBack)
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ fun WhosInNavGraph(
composable<Route.Home> {
HomeScreen(
modifier = modifier,
onNavigateBack = { navController.navigateUp() },
onNavigateToMyPage = {
navController.navigate(Route.MyPage)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.http.encodedPath
import io.ktor.serialization.kotlinx.json.json
Expand Down Expand Up @@ -47,31 +48,42 @@ object HttpClientFactory {
install(Auth){
bearer {
loadTokens {
val accessToken = tokenManager.getAccessToken() ?: "no_token"
val accessToken = tokenManager.getAccessToken() ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
}
Comment on lines +51 to 54
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 토큰을 즉시 제거해주세요

loadTokens 기본값으로 실제 JWT를 넣어 둔 상태입니다. 이는 비공개 자격 증명이 저장소에 노출되는 심각한 보안 사고이자, 토큰이 없어도 항상 해당 계정으로 인증 요청을 날리게 만들어 시스템 오용 위험까지 초래합니다. 토큰이 없을 때는 전송을 건너뛰도록 처리하고, 하드코드된 문자열을 삭제해 주세요.

-                        val accessToken = tokenManager.getAccessToken() ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
-                        val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
-                        BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
+                        val accessToken = tokenManager.getAccessToken()
+                        val refreshToken = tokenManager.getRefreshToken()
+                        if (accessToken.isNullOrBlank() || refreshToken.isNullOrBlank()) {
+                            return@loadTokens null
+                        }
+                        BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
📝 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)
}
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
if (accessToken.isNullOrBlank() || refreshToken.isNullOrBlank()) {
return@loadTokens null
}
BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 51-51: 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 51-54, remove the hardcoded JWT and the "no_token" default and
change the logic so that when tokenManager.getAccessToken() or getRefreshToken()
returns null you do not construct or return BearerTokens; instead return null
(or an empty/absent marker) so the caller can skip attaching Authorization
headers. Delete the literal JWT string and "no_token", make the function return
a nullable BearerTokens (or Optional-equivalent) and ensure callers handle the
null case by not sending auth headers or requests that require auth.

sendWithoutRequest { request ->
val host = "https://"+request.url.host+"/"
val path = request.url.encodedPath
val pathWithNoAuth = listOf(
"jokes",
"users/signup",
"users/find-password",
"auth/login",
"auth/email",
"auth/email/validation"
)
// 결과가 true면 Authorization 헤더 추가, false면 제거
if(host != BASE_URL){
val requestHost = request.url.host
val baseHost = try {
Url(BASE_URL).host
} catch (e: Exception) {
// BASE_URL 형식이 잘못되었을 경우를 대비한 예외 처리
null
}

// 요청하는 API의 host가 우리 서버의 host와 다르면 외부 API로 간주하여 토큰을 보내지 않음
if (requestHost != baseHost) {
println("External API - No Auth")
false
}else{
// pathWithNoAuth에 있는 경로에는 Authorization 헤더 제외
} else {
// 우리 서버로 요청하는 경우, 인증이 필요 없는 경로인지 확인
val path = request.url.encodedPath
val pathWithNoAuth = listOf(
"jokes",
"users/signup",
"users/find-password",
"auth/login",
"auth/email",
"auth/email/validation",
"member/reissue" // 토큰 재발급 요청 자체에는 만료된 액세스 토큰을 보내면 안 됨
)

val isNoAuthPath = pathWithNoAuth.any { noAuthPath ->
path.startsWith(noAuthPath) || path.contains(noAuthPath)
path.contains(noAuthPath)
}
println("isNoAuthPath: $isNoAuthPath")

// isNoAuthPath가 true이면 인증이 필요 없는 경로 -> 헤더를 보내지 않음 (false 반환)
// isNoAuthPath가 false이면 인증이 필요한 경로 -> 헤더를 보냄 (true 반환)
!isNoAuthPath
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.whosin.client.data.dto.response

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

@Serializable
data class ClubPresencesResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: ClubPresencesData
)

@Serializable
data class ClubPresencesData(
@SerialName("clubName")
val clubName: String,
@SerialName("presentMembers")
val presentMembers: List<PresentMembers>
)

@Serializable
data class PresentMembers(
@SerialName("userName")
val userName: String,
@SerialName("isMe")
val isMe: Boolean
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,96 @@
package org.whosin.client.data.remote

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.http.isSuccess
import org.whosin.client.core.network.ApiResult
import org.whosin.client.data.dto.response.ClubPresencesResponseDto
import org.whosin.client.data.dto.response.MyClubResponseDto

class RemoteClubDataSource(
private val client : HttpClient
private val client: HttpClient
) {
suspend fun getMyClubs(): ApiResult<MyClubResponseDto> {
return try {
val response: HttpResponse = client.get(urlString = "clubs/my")

if (response.status.isSuccess()) {
ApiResult.Success(
data = response.body(),
statusCode = response.status.value
)
} else {
ApiResult.Error(
code = response.status.value,
message = "HTTP Error: ${response.status.value}"
)
}
} catch (t: Throwable) {
ApiResult.Error(message = t.message, cause = t)
}
}

suspend fun getPresentMembers(clubId: Int): ApiResult<ClubPresencesResponseDto> {
return try {
val response: HttpResponse = client.get(urlString = "clubs/$clubId/presences")

if (response.status.isSuccess()) {
ApiResult.Success(
data = response.body(),
statusCode = response.status.value
)
} else {
ApiResult.Error(
code = response.status.value,
message = "HTTP Error: ${response.status.value}"
)
}
} catch (t: Throwable) {
ApiResult.Error(message = t.message, cause = t)
}
}

suspend fun checkIn(clubId: Int): ApiResult<Unit> {
return try {
val response: HttpResponse = client.post(urlString = "clubs/$clubId/check-in")

if (response.status.isSuccess()) {
ApiResult.Success(
data = response.body(),
statusCode = response.status.value
)
} else {
ApiResult.Error(
code = response.status.value,
message = "HTTP Error: ${response.status.value}"
)
}
} catch (t: Throwable) {
ApiResult.Error(message = t.message, cause = t)
}
}

suspend fun checkOut(clubId: Int): ApiResult<Unit> {
return try {
val response: HttpResponse = client.delete(urlString = "clubs/$clubId/check-out")

if (response.status.isSuccess()) {
ApiResult.Success(
data = response.body(),
statusCode = response.status.value
)
} else {
ApiResult.Error(
code = response.status.value,
message = "HTTP Error: ${response.status.value}"
)
}
} catch (t: Throwable) {
ApiResult.Error(message = t.message, cause = t)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package org.whosin.client.data.repository

import org.whosin.client.core.network.ApiResult
import org.whosin.client.data.dto.response.ClubPresencesResponseDto
import org.whosin.client.data.dto.response.MyClubResponseDto
import org.whosin.client.data.remote.RemoteClubDataSource

class ClubRepository(
private val dataSource: RemoteClubDataSource
) {
suspend fun getMyClubs(): ApiResult<MyClubResponseDto> =
dataSource.getMyClubs()

suspend fun getPresentMembers(clubId: Int): ApiResult<ClubPresencesResponseDto> =
dataSource.getPresentMembers(clubId)

suspend fun checkIn(clubId: Int): ApiResult<Unit> =
dataSource.checkIn(clubId)

suspend fun checkOut(clubId: Int): ApiResult<Unit> =
dataSource.checkOut(clubId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.whosin.client.presentation.component

import androidx.compose.runtime.Composable

@Composable
expect fun CommonBackHandler(enabled: Boolean = true, onBack: () -> Unit)
Loading