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
2 changes: 1 addition & 1 deletion GAMSS/Sources/App/GAMSSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct GAMSSApp: App {

var body: some Scene {
WindowGroup {
LoginView(viewModel: LoginViewModel(loginUseCase: DefaultLoginUseCase(authRepository: DefaultAuthRepository(networkManager: NetworkManager.shared))))
LoginView(viewModel: LoginViewModel(loginUseCase: DefaultLoginUseCase(authRepository: DefaultAuthRepository(networkManager: NetworkManager.shared, tokenStorage: TokenStorage.shared))))
}
}
}
25 changes: 25 additions & 0 deletions GAMSS/Sources/Core/KeyChain/KeyChainAccount.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// KeyChainAccount.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

enum KeyChainAccount {
case accessToken
case refreshToken

var description: String {
return String(describing: self)
}

var keyChainClass: CFString {
switch self {
case .accessToken, .refreshToken:
return kSecClassGenericPassword
}
}
}

22 changes: 22 additions & 0 deletions GAMSS/Sources/Core/KeyChain/KeyChainError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// KeyChainError.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

enum KeyChainError: LocalizedError {
case unhandledError(status: OSStatus)
case itemNotFound

var errorDescription: String? {
switch self {
case .unhandledError(let status):
return "KeyChain unhandle Error: \(status)"
case .itemNotFound:
return "KeyChain item Not Found"
}
}
}
67 changes: 67 additions & 0 deletions GAMSS/Sources/Core/KeyChain/KeyChainManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// KeyChainManager.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

final class KeyChainManager {
static let shared = KeyChainManager()
private let service = Environment.bundleID

private init() { }

func create(account: KeyChainAccount, data: String) throws {
let query = [kSecAttrService: service,
kSecClass: account.keyChainClass,
kSecAttrAccount: account.description,
kSecValueData: data.data(using: .utf8, allowLossyConversion: false)!] as CFDictionary

Comment on lines +17 to +21

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

실패시 crash 발생이라 guard let을 사용하는 것도 좋을 것 같습니다

SecItemDelete(query)

let status = SecItemAdd(query, nil)

guard status == noErr else {
throw KeyChainError.unhandledError(status: status)
}
}

func read(account: KeyChainAccount) throws -> String {
let query = [kSecAttrService: service,
kSecClass: account.keyChainClass,
kSecAttrAccount: account.description,
kSecReturnData: true] as CFDictionary

var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query, &dataTypeRef)


guard status != errSecItemNotFound else {
throw KeyChainError.itemNotFound
}

if status == errSecSuccess,
let item = dataTypeRef as? Data,
let data = String(data: item, encoding: String.Encoding.utf8) {
return data
} else {
throw KeyChainError.unhandledError(status: status)
}
}

func delete(account: KeyChainAccount) throws {
let query = [kSecAttrService: service,
kSecClass: account.keyChainClass,
kSecAttrAccount: account.description] as CFDictionary

let status = SecItemDelete(query)

guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeyChainError.unhandledError(status: status)
}
}

}

30 changes: 30 additions & 0 deletions GAMSS/Sources/Core/LoginState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// LoginState.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

enum LoginState {
/// 로그인 안되어 있음
case notLoggedIn
/// 자동 로그인 설정이 되어있음, accessToken 갱신 필요
case autoLoginPending
/// 로그인 되어있음
case loggedIn

/// 현재 로그인 상태 반환
static var current: Self {
guard TokenStorage.shared.readToken(.refreshToken) != nil else {
return .notLoggedIn
}

guard TokenStorage.shared.readToken(.accessToken) != nil else {
return .autoLoginPending
}

return .loggedIn
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ protocol Endpoint {
var baseURLString: String { get }
var path: String { get }
var method: HTTPMethod { get }
var body: Encodable? { get }
}

extension Endpoint {
/// FIXME: - xcconfig로 변환 예정
var baseURLString: String {
return ""
}

var headers: [String: String] {
return [:]
return [
"Content-Type": "application/json",
"Accept": "application/json"
]
}

func asURLRequest() throws -> URLRequest {
Expand All @@ -39,6 +44,10 @@ extension Endpoint {
)
}

if let body {
request.httpBody = try JSONEncoder().encode(body)
}

return request
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,19 @@ final class NetworkManager: NetworkRequesting {
}

guard 200..<300 ~= response.statusCode else {
throw NetworkError.httpError(
statusCode: response.statusCode
)
let apiError = try? decoder.decode(
APIResponse<EmptyResponseDTO>.self,
from: data
).error

Log.error("""
❌ API Error
StatusCode: \(response.statusCode)
Code: \(apiError?.code ?? "UNKNOWN")
Message: \(apiError?.message ?? "No error message")
""")

throw NetworkError.httpError(statusCode: response.statusCode)
}

do {
Expand Down
19 changes: 19 additions & 0 deletions GAMSS/Sources/Data/DTO/APIResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// APIResponse.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

struct APIResponse<T: Decodable>: Decodable {
let success: Bool
let data: T
let error: ErrorResponse?
}
Comment thread
dlrjswns marked this conversation as resolved.

struct ErrorResponse: Decodable {
let code: String
let message: String
}
12 changes: 12 additions & 0 deletions GAMSS/Sources/Data/DTO/LoginRequestDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// LoginRequestDTO.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

struct LoginRequestDTO: Encodable {
let idToken: String
}
13 changes: 13 additions & 0 deletions GAMSS/Sources/Data/DTO/LoginResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// LoginResponseDTO.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

struct LoginResponseDTO: Decodable {
let accessToken: String
let refreshToken: String
}
33 changes: 33 additions & 0 deletions GAMSS/Sources/Data/Endpoint/AuthEndpoint.swift

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

모야랑 비슷하네요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@cchanmi 지금 Endpoint에서 헤더값에 디폴트로 넣고있는데 추후에 토큰값을 넣어야하는 경우에 따른 HeaderType도 넣으면 깔끔할꺼같아요 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// AuthEndpoint.swift
// GAMSS
//
// Created by 이건준 on 7/29/26.
//

import Foundation

enum AuthEndpoint: Endpoint {
case login(LoginRequestDTO)

var path: String {
switch self {
case .login:
return "/api/auth/login"
}
}

var method: HTTPMethod {
switch self {
case .login:
return .post
}
}

var body: Encodable? {
switch self {
case let .login(request):
return request
}
}
}
72 changes: 62 additions & 10 deletions GAMSS/Sources/Data/Repository/DefaultAuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,91 @@ import FirebaseAuth

final class DefaultAuthRepository: AuthRepository {
private let networkManager: NetworkRequesting
private let tokenStorage: TokenStorage

init(networkManager: NetworkRequesting) {
init(
networkManager: NetworkRequesting,
tokenStorage: TokenStorage
) {
self.networkManager = networkManager
self.tokenStorage = tokenStorage
}

// FIXME: - 로그인 시 올바른 응답값으로 수정 필요
func login(
with socialType: SocialType,
credential: ASAuthorizationAppleIDCredential,
nonce: String
) async throws {
switch socialType {
case .apple:
try await loginWithApple(credential: credential, nonce: nonce)
try await loginWithApple(
credential: credential,
nonce: nonce
)
}
}

private func loginWithApple(
credential: ASAuthorizationAppleIDCredential,
nonce: String
) async throws {
guard let identityToken = credential.identityToken,
let idToken = String(
data: identityToken,
encoding: .utf8
)
else { return }
let firebaseIdToken = try await signInFirebase(
credential: credential,
nonce: nonce
)

try await login(firebaseIdToken: firebaseIdToken)
}


private func signInFirebase(
credential: ASAuthorizationAppleIDCredential,
nonce: String
) async throws -> String {
guard let identityToken = credential.identityToken else {
throw AuthError.missingIdentityToken
}

guard let idToken = String(
data: identityToken,
encoding: .utf8
) else {
throw AuthError.invalidIdentityToken
}

let firebaseCredential = OAuthProvider.appleCredential(
withIDToken: idToken,
rawNonce: nonce,
fullName: credential.fullName
)
Log.debug("토큰: \(idToken), 이름: \(credential.fullName), 암호: \(nonce)")

do {
let authResult = try await Auth.auth().signIn(
with: firebaseCredential
)

return try await authResult.user.getIDToken()

} catch {
throw AuthError.firebaseSignInFailed(error)
}
}

private func login(
firebaseIdToken: String
) async throws {
let response = try await networkManager.request(
AuthEndpoint.login(.init(idToken: firebaseIdToken)),
responseType: APIResponse<LoginResponseDTO>.self
)

do {
try tokenStorage.createTokens(
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
)
} catch {
throw AuthError.tokenStorageFailed(error)
}
}
}
Loading
Loading