-
Notifications
You must be signed in to change notification settings - Fork 4
[API] home api 통신 #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
40224bc
565bf9b
15b05f1
08bee90
6b342ba
918fe9b
3bb10c1
45c1620
0d2f7fe
b38a1ab
e2cae8d
6af3b98
021217a
42218bd
3df6b5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하드코드된 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
Suggested change
🧰 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 |
||||||||||||||||||||||
| 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 | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Verification inconclusive
평문 트래픽 허용은 보안 위험이 있습니다.
android:usesCleartextTraffic="true"설정은 암호화되지 않은 HTTP 통신을 허용합니다. iOS 측Info.plist와 동일하게, 이는 중간자 공격에 노출될 수 있는 보안 위험이 있습니다.개발 단계라면 임시로 사용할 수 있지만, 프로덕션 릴리스 전에는 반드시:
false로 변경하거나 제거해야 합니다. Android 9(API 28) 이상에서는 기본적으로 cleartext 트래픽이 차단되므로, 보안을 위해 HTTPS 사용을 권장합니다.
AndroidManifest.xml(14행) cleartext 트래픽 허용 설정 제거
android:usesCleartextTraffic="true"는 중간자 공격 등에 취약하므로 개발용 임시 설정을 제외하고 프로덕션 전에는 false로 변경하거나 제거하고, 백엔드 API를 HTTPS로 전환해야 합니다.🤖 Prompt for AI Agents