-
Notifications
You must be signed in to change notification settings - Fork 2
[Feat] #198 Apple Login 구현 + AuthInterceptor 적용 #201
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
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3276de9
[Feat] #198 Apple Login 버튼 추가
kwonseokki ba2d80f
[Fix] #198 Network Header에서 userID 제거
kwonseokki df78e6d
[Add] #198 AppleLoginDTO 추가
kwonseokki 0cf1689
[Feat] #198 Apple LoginAPI 추가
kwonseokki 9537ce2
[Fix] #198 병합 충돌 해결
kwonseokki 7e53c67
[Add] #198 AppleLoginResponseDto 추가
kwonseokki baacc22
[Add] #198 KeychainManager 추가
kwonseokki 4a82f1f
[Feat] #198 로그인 체크 로직 추가
kwonseokki 48c697f
[Feat] #198 AuthInterceptor 추가 및 토큰 재발급 기능 구현
kwonseokki 13172d3
[Feat] #198 AuthManager 토큰 재발급 로직 추가
kwonseokki af0b05f
[Fix] #198 불필요한 네트워크 headerType 제거
kwonseokki 7e11371
[Feat] #198 토큰 재발급 테스트용 API 추가
kwonseokki a23da2b
Merge remote-tracking branch 'refs/remotes/origin/feat/#198'
kwonseokki 293ddf7
[Fix] #198 애플 로그인 서버 명세 변경 반영
seokki2 aa12284
[Fix] #198 온보딩 이미지 교체
seokki2 90f0c22
[Fix] #201 AppleLogin 서버 명세 변경 반영
seokki2 1c12cc2
[Feat] #198 신규유저 여부에 따라서 화면 분기 처리
seokki2 b563664
[Feat] #198 유저 정보입력 화면이동 처리
seokki2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // | ||
| // KeychainManager.swift | ||
| // DOKI | ||
| // | ||
| // Created by a on 12/7/25. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum KeychainError: Error { | ||
| case noPassword | ||
| case unhandledError(status: OSStatus) | ||
| case unexpectedPasswordData | ||
|
|
||
| var message: String { | ||
| switch self { | ||
| case .noPassword: return "No password available." | ||
| case .unexpectedPasswordData: return "Expected data, but found none." | ||
| case .unhandledError(let status): return "Unhandled error with status: \(status)" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum KeychainName: String { | ||
| case accessToken | ||
| case refreshToken | ||
| } | ||
|
|
||
| struct KeychainManager { | ||
|
|
||
| /// Keychain 저장소에서 key에 해당하는 값을 추가 | ||
| static func create<T: Codable>(_ key: KeychainName, _ value: T) throws { | ||
| do { | ||
| let valueData = try JSONEncoder().encode(value) | ||
| let query: NSDictionary = [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key.rawValue, | ||
| kSecValueData: valueData | ||
| ] | ||
| SecItemDelete(query) | ||
|
|
||
| let status = SecItemAdd(query, nil) | ||
| guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } | ||
| } catch { | ||
| throw KeychainError.unexpectedPasswordData | ||
| } | ||
| } | ||
|
|
||
| /// Keychain 저장소에서 key에 해당하는 값을 검색 | ||
| @discardableResult | ||
| static func read(_ key: KeychainName) throws -> String? { | ||
| let query: NSDictionary = [kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key.rawValue, | ||
| kSecMatchLimit: kSecMatchLimitOne, | ||
| kSecReturnData: true] | ||
| var item: CFTypeRef? | ||
| let status = SecItemCopyMatching(query as CFDictionary, &item) | ||
| guard status != errSecItemNotFound else { throw KeychainError.noPassword } | ||
| if status == errSecSuccess { | ||
| if let retrievedItem = item as? Data { | ||
| let returnValue = String(data: retrievedItem, encoding: String.Encoding.utf8) | ||
| return returnValue | ||
| } else { | ||
| return nil | ||
| } | ||
| } else { | ||
| throw KeychainError.unexpectedPasswordData | ||
| } | ||
| } | ||
|
|
||
| /// key에 해당하는 값을 삭제 | ||
| static func delete(_ key: KeychainName) throws { | ||
| let query: NSDictionary = [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key.rawValue | ||
| ] | ||
| let status = SecItemDelete(query) | ||
| guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } | ||
| } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // | ||
| // AuthInterceptor.swift | ||
| // DOKI | ||
| // | ||
| // Created by a on 12/9/25. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| import Moya | ||
| import Alamofire | ||
|
|
||
| final class AuthInterceptor: RequestInterceptor { | ||
| static let shared = AuthInterceptor() | ||
|
|
||
| private init() {} | ||
|
|
||
| // 네트워크 요청하기전 헤더에 accessToken 추가 | ||
| func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) { | ||
| var request = urlRequest | ||
|
|
||
| if request.url?.absoluteString.contains("auth/refresh") == true { | ||
| completion(.success(request)) | ||
| return | ||
| } | ||
| if let accessToken = AuthManager.shared.accessToken { | ||
| request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") | ||
| } | ||
|
|
||
| completion(.success(request)) | ||
| } | ||
|
|
||
| func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { | ||
| // 401인 경우가 아니라면 종료 | ||
| guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else { | ||
| completion(.doNotRetryWithError(error)) | ||
| return | ||
| } | ||
|
|
||
| // refreshToken 가져오기 없다면 종료 | ||
| guard let refreshToken = AuthManager.shared.refreshToken?.replacingOccurrences(of: "\"", with: "") else { | ||
| completion(.doNotRetry) | ||
| AuthManager.shared.logout() | ||
| return | ||
| } | ||
|
|
||
| // 토큰 재발급 API 호출 & 토큰 교체 | ||
| var refreshRequest = URLRequest(url: URL(string: Config.baseURL + "auth/refresh")!) | ||
| refreshRequest.httpMethod = "POST" | ||
| refreshRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") | ||
|
|
||
| let requestBody = try? JSONSerialization.data(withJSONObject: ["refreshToken": refreshToken, "deviceId": "doki-service"]) | ||
| refreshRequest.httpBody = requestBody | ||
| let defaultSession = URLSession(configuration: .default) | ||
|
|
||
| defaultSession.dataTask(with: refreshRequest) { (data: Data?, response: URLResponse?, error: Error?) in | ||
| // 에러 발생시 재요청x | ||
| guard error == nil else { | ||
| completion(.doNotRetry) | ||
| AuthManager.shared.logout() | ||
| return | ||
| } | ||
|
|
||
| guard let data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else { | ||
| completion(.doNotRetry) | ||
| AuthManager.shared.logout() | ||
| return | ||
| } | ||
|
|
||
| // 토큰 재발급 요청 성공 | ||
| do { | ||
| let response = try JSONDecoder().decode(AppleLoginResponseDTO.self, from: data) | ||
| // 토큰 재발급 | ||
| AuthManager.shared.reissueToken( | ||
| accessToken: response.accessToken, | ||
| refreshToken: response.refreshToken | ||
| ) | ||
| // 재요청 | ||
| print("토큰 재발급 성공 - 재요청") | ||
| completion(.retry) | ||
| } catch { | ||
| print("토큰 재발급 실패 - 로그아웃") | ||
| completion(.doNotRetryWithError(error)) | ||
| AuthManager.shared.logout() | ||
| } | ||
| }.resume() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // | ||
| // AppleLoginRequestDTO.swift | ||
| // DOKI | ||
| // | ||
| // Created by a on 12/7/25. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| struct AppleLoginRequestDTO: Encodable { | ||
| let authorizationCode: String | ||
| let deviceId: String | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
userId 안 쓰는 거 마음이 편안해지네요