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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ graph.dot
*.log

# Local Xcode derived data for CLI builds
.derivedData/
# .derivedData, .derivedData-pr3, .derivedData-keycheck 등 CLI 임시 경로 전부 무시
.derivedData*/

# macOS junk
.DS_Store
Expand Down
20 changes: 19 additions & 1 deletion .package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"originHash" : "a233e486feeee6851825f048f7a7b0b0f1dc2fd69a98db3f78b3507baa03d09a",
"originHash" : "04f9c7beed1c00c645a6d4cc85b6dc9f57eb3b4efe2300fbb9aba9ab4b820bc7",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "7595cbcf59809f9977c5f6378500de2ad73b7ddb",
"version" : "5.12.0"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
Expand All @@ -10,6 +19,15 @@
"version" : "1.2.0"
}
},
{
"identity" : "kakao-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kakao/kakao-ios-sdk",
"state" : {
"revision" : "2a68ca01e2d7900a1559b31d0d59843837f130f2",
"version" : "2.28.0"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
Expand Down
11 changes: 11 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ excluded:
- Derived
- DerivedData
- .derivedData
- .derivedData-app
- .derivedData-app2
- .derivedData-coresocialauth
- .derivedData-data
- .derivedData-data2
- .derivedData-feature
- .derivedData-feature2
- .derivedData-feature3
- .derivedData-networklog
- .derivedData-rename
- .derivedData-rename-app
- .git
- Mozi.xcworkspace
- "**/*.xcodeproj"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
Core/{Network,Storage}
Core/{Network,Storage,SocialAuth}
Data/
Feature/
App/
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
Core/{Network,Storage}
Core/{Network,Storage,SocialAuth}
Data/
Feature/
App/
Expand Down
1 change: 1 addition & 0 deletions Config/Example.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Debug.xcconfig / Release.xcconfig 로 복사해서 사용
// https:/$()/... 형태는 // 가 주석으로 파싱되지 않게 하기 위함
API_BASE_URL =
KAKAO_NATIVE_APP_KEY =
10 changes: 10 additions & 0 deletions Projects/App/Mozi.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>
4 changes: 3 additions & 1 deletion Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ let project = ProjectFactory.app(
.domain,
.coreNetwork,
.coreStorage,
.coreSocialAuth,
.sharedLogger,
.sharedUtils,
.sharedDesignSystem,
.thirdParty,
.thirdPartyUI,
.thirdPartyCore,
]
],
entitlements: .file(path: "Mozi.entitlements")
)
27 changes: 26 additions & 1 deletion Projects/App/Sources/DI/AppBootstrap.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreSocialAuth
import Foundation
import SharedDesignSystem
import SharedLogger
Expand All @@ -8,9 +9,33 @@ enum AppBootstrap {
static func run() {
_ = DesignSystemFontRegistration.registerIfNeeded()
let infra = InfraContainer.live()
let kakaoAppKey = requireKakaoNativeAppKeyIfNeeded(
infra.configuration.kakaoNativeAppKey
)
let socialConfig = SocialAuthConfiguration(
kakaoAppKey: kakaoAppKey
)
KakaoAuthBootstrap.initializeIfNeeded(appKey: socialConfig.kakaoAppKey)
prepareDependencies {
Dependencies.register(&$0, infra: infra)
Dependencies.register(&$0, infra: infra, socialConfig: socialConfig)
}
Logger.shared.info("App bootstrap completed", category: .general)
}

/// Debug 에서는 카카오 키 누락을 부트 시점에 즉시 실패시킨다.
private static func requireKakaoNativeAppKeyIfNeeded(_ key: String?) -> String? {
#if DEBUG
guard let key, key.isEmpty == false else {
preconditionFailure(
"""
Missing KAKAO_NATIVE_APP_KEY.
Copy Config/Example.xcconfig to Config/Debug.xcconfig and set the native app key for this scheme.
"""
)
}
return key
#else
return key
#endif
}
}
4 changes: 3 additions & 1 deletion Projects/App/Sources/DI/AppConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ struct AppConfiguration: Sendable {
let baseURL: URL
let bundleID: String
let displayName: String
let kakaoNativeAppKey: String?

static func make() -> AppConfiguration {
AppConfiguration(
baseURL: AppInfo.apiBaseURL,
bundleID: AppInfo.bundleID,
displayName: AppInfo.displayName
displayName: AppInfo.displayName,
kakaoNativeAppKey: AppInfo.kakaoNativeAppKey
)
}
}
10 changes: 8 additions & 2 deletions Projects/App/Sources/DI/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import CoreSocialAuth
import Data
import Domain
import ThirdParty

enum Dependencies {
@MainActor
static func register(
_ values: inout DependencyValues,
infra: InfraContainer
infra: InfraContainer,
socialConfig: SocialAuthConfiguration
) {
let socialAuthServices = SocialAuthServiceFactory().make(
configuration: socialConfig
)
values.authClient = .live(
baseURL: infra.configuration.baseURL,
keychain: infra.keychain,
oauthServices: OAuthServiceFactory.makeStub()
socialAuthServices: socialAuthServices
)
}
}
4 changes: 1 addition & 3 deletions Projects/App/Sources/DI/InfraContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ extension InfraContainer {
let configuration = AppConfiguration.make()
return InfraContainer(
configuration: configuration,
userDefaults: DefaultUserDefaultsStorage(
suiteName: configuration.bundleID
),
userDefaults: DefaultUserDefaultsStorage(),
keychain: DefaultKeychainStorage(
service: configuration.bundleID
)
Expand Down
7 changes: 7 additions & 0 deletions Projects/App/Sources/MoziApp.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreSocialAuth
import Feature
import SwiftUI
import ThirdParty
Expand All @@ -14,6 +15,12 @@ struct MoziApp: App {
WindowGroup {
CompositionRoot.rootView(store: store)
.preferredColorScheme(.dark)
.onOpenURL { url in
if KakaoAuthRedirectHandler.handle(url: url) {
return
}
store.send(.appCoordinator(.deepLinkReceived(url)))
}
}
}
}
69 changes: 52 additions & 17 deletions Projects/Core/Network/Sources/Logging/NetworkLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ enum NetworkLog {

#if DEBUG
if let body = request.httpBody, let bodyText = String(data: body, encoding: .utf8) {
message += "\nBody: \(redact(bodyText))"
message += "\nBody:\n\(formattedBody(bodyText))"
}
#endif

Logger.shared.info(redact(message), category: .network)
Logger.shared.info(message, category: .network)
}

static func response(
Expand All @@ -27,7 +27,7 @@ enum NetworkLog {

#if DEBUG
if let bodyText = String(data: data, encoding: .utf8), !bodyText.isEmpty {
message += "\nBody: \(redact(bodyText))"
message += "\nBody:\n\(formattedBody(bodyText))"
}
#endif

Expand All @@ -53,23 +53,58 @@ enum NetworkLog {
return components.string ?? url.absoluteString
}

/// JSON body 는 여러 줄로 정리한 뒤 민감 값만 가린다.
static func formattedBody(_ text: String) -> String {
let pretty = prettyPrintedJSON(text) ?? text
return redact(pretty)
}

static func prettyPrintedJSON(_ text: String) -> String? {
guard let data = text.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data),
let prettyData = try? JSONSerialization.data(
withJSONObject: object,
options: [.prettyPrinted, .sortedKeys]
),
let pretty = String(data: prettyData, encoding: .utf8) else {
return nil
}
return pretty
}

/// 민감 값만 가리고 필드명은 남긴다.
static func redact(_ text: String) -> String {
var output = text
let patterns = [
#"Bearer\s+[A-Za-z0-9\-._~+/]+=*"#,
#"(accessToken|refreshToken|Authorization)"\s*:\s*"[^"]+""#,
]
for pattern in patterns {
if let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) {
let range = NSRange(output.startIndex..<output.endIndex, in: output)
output = regex.stringByReplacingMatches(
in: output,
options: [],
range: range,
withTemplate: "[REDACTED]"
)
}

// Authorization: Bearer <token>
if let bearerRegex = try? NSRegularExpression(
pattern: #"Bearer\s+[A-Za-z0-9\-._~+/]+=*"#,
options: [.caseInsensitive]
) {
let range = NSRange(output.startIndex..<output.endIndex, in: output)
output = bearerRegex.stringByReplacingMatches(
in: output,
options: [],
range: range,
withTemplate: "Bearer [REDACTED]"
)
}

// JSON string fields: keep key, mask value
// pretty print 공백을 허용한다.
if let fieldRegex = try? NSRegularExpression(
pattern: #""(accessToken|refreshToken|Authorization|identityToken)"\s*:\s*"[^"]*""#,
options: [.caseInsensitive]
) {
let range = NSRange(output.startIndex..<output.endIndex, in: output)
output = fieldRegex.stringByReplacingMatches(
in: output,
options: [],
range: range,
withTemplate: #""$1" : "[REDACTED]""#
)
}

return output
}
}
30 changes: 30 additions & 0 deletions Projects/Core/Network/Tests/NetworkLogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,34 @@ final class NetworkLogTests: XCTestCase {
XCTAssertFalse(sanitized.contains("page=1"))
XCTAssertFalse(sanitized.contains("#top"))
}

func test_formattedBody가_JSON을_여러줄로_정리하고_토큰_값만_가린다() {
let body = #"{"accessToken":"abc","refreshToken":"def","isNewUser":false,"profileCompleted":false}"#

let formatted = NetworkLog.formattedBody(body)

XCTAssertTrue(formatted.contains("\n"))
XCTAssertTrue(formatted.contains(#""accessToken" : "[REDACTED]""#))
XCTAssertTrue(formatted.contains(#""refreshToken" : "[REDACTED]""#))
XCTAssertTrue(formatted.contains(#""isNewUser" : false"#))
XCTAssertTrue(formatted.contains(#""profileCompleted" : false"#))
XCTAssertFalse(formatted.contains("abc"))
XCTAssertFalse(formatted.contains("def"))
}

func test_redact가_identityToken과_Bearer_값을_가린다() {
let text = """
Authorization: Bearer secret-token
{
"identityToken" : "jwt.header.payload"
}
"""

let redacted = NetworkLog.redact(text)

XCTAssertTrue(redacted.contains("Bearer [REDACTED]"))
XCTAssertTrue(redacted.contains(#""identityToken" : "[REDACTED]""#))
XCTAssertFalse(redacted.contains("secret-token"))
XCTAssertFalse(redacted.contains("jwt.header.payload"))
}
}
11 changes: 11 additions & 0 deletions Projects/Core/SocialAuth/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ProjectDescription
import ProjectDescriptionHelpers

let project = ProjectFactory.framework(
.coreSocialAuth,
dependencies: [
.sharedLogger,
.thirdPartyCore,
],
includesTests: true
)
24 changes: 24 additions & 0 deletions Projects/Core/SocialAuth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# CoreSocialAuth

## 책임
- 소셜 identity provider credential 획득 (Kakao access token, Apple identity token)
- configuration / factory / bootstrap / redirect helper

## 금지
- Domain import
- repository / endpoint / AuthError
- App lifecycle 소유

## 의존
- 허용: ThirdPartyCore, SharedLogger, AuthenticationServices/UIKit
- 금지: Domain, Data, Feature, App

## 주요 진입점
- `SocialAuthServiceFactory`
- `SocialAuthConfiguration` / `SocialAuthServices` / `SocialAuthService`
- `KakaoAuthBootstrap`
- `KakaoAuthRedirectHandler`
- `KakaoSocialAuthService` / `AppleSocialAuthService`

## 테스트 포인트
- factory notConfigured (kakao key 없음/빈 값)
Loading