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: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Configuration/

### Firebase ###
GoogleService-Info*.plist
functions
.firebaserc
firebase.json

### macOS ###
.DS_Store
Expand Down
4 changes: 4 additions & 0 deletions Mark-In/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>RevokeTokenURL</key>
<string>$(REVOKE_TOKEN_URL)</string>
<key>GetRefreshTokenURL</key>
<string>$(GET_REFRESH_TOKEN_URL)</string>
<key>GIDClientID</key>
<string>$(GID_CLIENT)</string>
<key>CFBundleURLTypes</key>
Expand Down
19 changes: 14 additions & 5 deletions Mark-In/Sources/App/AuthUserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@

import Foundation

import FirebaseAuth
import Util

struct AuthUser {
struct AuthUser: Codable {
let id: String
let name: String
let email: String
let provider: SocialSignInProvider
Comment on lines +14 to +18
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

keychain에 저장하기 위해 Codable 프로토콜을 채택했습니다.

}

protocol AuthUserManager {
Expand All @@ -25,22 +28,28 @@ protocol AuthUserManager {

@Observable
final class AuthUserManagerImpl: AuthUserManager {

private let keychainStore: KeychainStore

var user: AuthUser?

init() {
init(keychainStore: KeychainStore) {
self.keychainStore = keychainStore
self.load()
}

func load() {
guard let currentUser = Auth.auth().currentUser else { return }
self.user = AuthUser(id: currentUser.uid)
let user: AuthUser? = try? keychainStore.load(forKey: "authUser")
self.user = user
}

func save(_ user: AuthUser) {
try? keychainStore.save(user, forKey: "authUser")
self.user = user
}

func clear() {
try? keychainStore.delete(forKey: "authUser")
user = nil
}
}
22 changes: 22 additions & 0 deletions Mark-In/Sources/App/Config.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Config.swift
// Mark-In
//
// Created by 이정동 on 5/14/25.
//

import Foundation

struct Config {
enum Key: String {
case getRefreshTokenURL = "GetRefreshTokenURL"
case revokeTokenURL = "RevokeTokenURL"
}

static func value(forKey: Self.Key) -> String {
guard let value = Bundle.main.object(forInfoDictionaryKey: forKey.rawValue) as? String else {
fatalError("\(forKey.rawValue) not set")
}
return value
}
}
19 changes: 18 additions & 1 deletion Mark-In/Sources/App/DIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ extension DIContainer {
func registerDependencies() {

/// Core
let keychainStore: KeychainStore = KeychainStoreImpl()
let linkMetadataProvider: LinkMetadataProvider = LinkMetadataProviderImpl()
let authUserManager: AuthUserManager = AuthUserManagerImpl()
let authUserManager: AuthUserManager = AuthUserManagerImpl(
keychainStore: keychainStore
)

register(linkMetadataProvider)
register(authUserManager)
Expand All @@ -66,10 +69,24 @@ extension DIContainer {
let generateFolderUseCase: GenerateFolderUseCase = GenerateFolderUseCaseImpl(
folderRepository: folderRepository
)
let signInUseCase: SignInUseCase = SignInUseCaseImpl(
keychainStore: keychainStore,
authUserManager: authUserManager
)
let signOutUseCase: SignOutUseCase = SignOutUseCaseImpl(
authUserManager: authUserManager
)
let withdrawalUseCase: WithdrawalUseCase = WithdrawalUseCaseImpl(
keychainStore: keychainStore,
authUserManager: authUserManager
)

register(fetchLinkListUseCase)
register(fetchFolderListUseCase)
register(generateLinkUseCase)
register(generateFolderUseCase)
register(signInUseCase)
register(signOutUseCase)
register(withdrawalUseCase)
}
}
88 changes: 88 additions & 0 deletions Mark-In/Sources/App/KeyChainStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// KeyChainStore.swift
// Mark-In
//
// Created by 이정동 on 5/14/25.
//




// TODO: Core 모듈로 이동

import Foundation
import Security

enum KeychainError: Error {
case encodingFailed
case decodingFailed
case unexpectedStatus(OSStatus)
}

protocol KeychainStore {
func save<T: Codable>(_ value: T, forKey key: String) throws
func load<T: Codable>(forKey key: String) throws -> T?
func delete(forKey key: String) throws
}

struct KeychainStoreImpl: KeychainStore {

func save<T: Codable>(_ value: T, forKey key: String) throws {
let data: Data
do {
data = try JSONEncoder().encode(value)
} catch {
throw KeychainError.encodingFailed
}

// 기존 항목 삭제 후 저장
try delete(forKey: key)

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]

let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}

func load<T: Codable>(forKey key: String) throws -> T? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)

guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}

guard let data = result as? Data else { return nil }

do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw KeychainError.decodingFailed
}
}

func delete(forKey key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]

let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
}
6 changes: 2 additions & 4 deletions Mark-In/Sources/App/Mark_InApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,8 @@ extension Mark_InApp {
#endif

guard let filePath = Bundle.main.path(forResource: resource, ofType: "plist"),
let options = FirebaseOptions(contentsOfFile: filePath)
else {
print("Firebase: \(resource) not found or invalid.")
return
let options = FirebaseOptions(contentsOfFile: filePath) else {
fatalError("Firebase: \(resource) not found or invalid.")
}

FirebaseApp.configure(options: options)
Expand Down
94 changes: 94 additions & 0 deletions Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

현재 외부 시스템과 통신하는 코드까지 유스케이스에 하드코딩이 되어있는 상태입니다.
후에 Auth 모듈을 구현하면서 분리해볼 예정입니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// SignInUseCaseImpl.swift
// Mark-In
//
// Created by 이정동 on 5/15/25.
//

import Foundation

import FirebaseAuth

import Util

struct SignInUseCaseImpl: SignInUseCase {

private let keychainStore: KeychainStore
private let authUserManager: AuthUserManager

init(
keychainStore: KeychainStore,
authUserManager: AuthUserManager
) {
self.keychainStore = keychainStore
self.authUserManager = authUserManager
}

func signIn(using info: AppleSignInInfo) async throws {

Choose a reason for hiding this comment

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

execute

/// 애플 로그인 정보에 필요한 정보들이 누락되는 경우
guard let nonce = info.nonce,
let appleIDToken = info.idCredential.identityToken,
let idTokenString = String(data: appleIDToken, encoding: .utf8),
let authorizationCode = info.idCredential.authorizationCode,
let codeString = String(data: authorizationCode, encoding: .utf8) else {
throw SignInError.missingData
}

// TODO: 아래 코드들을 Core(Auth) 모듈에서 처리하도록 리팩토링
/// 애플 서버에 Refresh Token 요청
let urlString = "https://\(Config.value(forKey: .getRefreshTokenURL))/getRefreshToken?code=\(codeString)"
let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!

Choose a reason for hiding this comment

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

안전하게 처리 필요


let (data, _) = try await URLSession.shared.data(from: url)
let refreshToken = String(data: data, encoding: .utf8) ?? ""

/// Refresh Token 저장
try keychainStore.save(refreshToken, forKey: "refreshToken")

/// Firebase 인증 요청을 위한 AuthCredential 생성
let credential = OAuthProvider.appleCredential(
withIDToken: idTokenString,
rawNonce: nonce,
fullName: info.idCredential.fullName
)

/// Firebase 인증 요청
let response = try await Auth.auth().signIn(with: credential)

/// AuthUserManager에 유저 데이터 저장
let authUser = AuthUser(
id: response.user.uid,
name: response.user.displayName ?? "-",
email: response.user.email ?? "-",
provider: .apple
)
authUserManager.save(authUser)
}

func signIn(using info: GoogleSignInInfo) async throws {
/// 로그인에 필요한 정보(IDToken)가 누락된 경우
guard let idToken = info.user.idToken?.tokenString else {
throw SignInError.missingData
}

// TODO: 아래 코드들을 Core(Auth) 모듈에서 처리하도록 리팩토링
/// Firebase 인증 요청을 위한 AuthCredential 생성
let credential = GoogleAuthProvider.credential(
withIDToken: idToken,
accessToken: info.user.accessToken.tokenString
)

/// Firebase 인증 요청
let response = try await Auth.auth().signIn(with: credential)

/// AuthUserManager에 유저 데이터 저장
let authUser = AuthUser(
id: response.user.uid,
name: response.user.displayName ?? "-",
email: response.user.email ?? "-",
provider: .google
)
authUserManager.save(authUser)
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Auth 모듈 분리 후 리팩토링 예정

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// SignOutUseCaseImpl.swift
// Mark-In
//
// Created by 이정동 on 5/15/25.
//

import Foundation

import FirebaseAuth
import GoogleSignIn

struct SignOutUseCaseImpl: SignOutUseCase {

private let authUserManager: AuthUserManager

init(authUserManager: AuthUserManager) {
self.authUserManager = authUserManager
}

func execute() {
// TODO: Core(Auth) 모듈에서 처리하도록 리팩토링
try? Auth.auth().signOut()

/// 구글 사용자인 경우
if authUserManager.user?.provider == .google {
GIDSignIn.sharedInstance.signOut()
}
Comment on lines +26 to +28

Choose a reason for hiding this comment

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

switch

apple: break // 애플 로그아웃 API 제공하지 않음


authUserManager.clear()
}
}
Loading
Loading