Skip to content
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

🔀 :: 액세스 토큰 자동 Refresh 로직 역할 분리 #7

Merged
merged 10 commits into from
Apr 17, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -29,74 +29,32 @@ open class BaseRemoteDataSource<Endpoint: DotoriEndpoint> {
}

public func request<T: Decodable>(_ endpoint: Endpoint, dto: T.Type) -> AnyPublisher<T, Error> {
requestPublisher(endpoint)
performRequest(endpoint)
.map(\.data)
.decode(type: dto, decoder: decoder)
.mapError { $0 as Error }
.eraseToAnyPublisher()
}

public func request(_ endpoint: Endpoint) -> AnyPublisher<Void, Error> {
requestPublisher(endpoint)
performRequest(endpoint)
.map { _ in return }
.eraseToAnyPublisher()
}

private func requestPublisher(_ endpoint: Endpoint) -> AnyPublisher<DataResponse, Error> {
return checkIsEndpointNeedsAuthorization(endpoint) ?
authorizedRequest(endpoint) :
defaultRequest(endpoint)
}
}

private extension BaseRemoteDataSource {
func defaultRequest(_ endpoint: Endpoint) -> AnyPublisher<DataResponse, Error> {
func performRequest(_ endpoint: Endpoint) -> AnyPublisher<DataResponse, Error> {
client.requestPublisher(endpoint)
.retry(maxRetryCount)
.timeout(45, scheduler: RunLoop.main)
.timeout(RunLoop.SchedulerTimeType.Stride(endpoint.timeout), scheduler: RunLoop.main)
.mapError {
if case let .underlying(err) = $0,
let emdpointError = err as? EmdpointError,
case let .statusCode(response) = emdpointError,
if case let .statusCode(response) = $0,
let httpResponse = response.response as? HTTPURLResponse {
return endpoint.errorMapper?[httpResponse.statusCode] ?? $0 as Error
}
return $0 as Error
}
.eraseToAnyPublisher()
}

func authorizedRequest(_ endpoint: Endpoint) -> AnyPublisher<DataResponse, Error> {
if checkTokenIsExpired() {
return reissueToken()
.retry(maxRetryCount)
.flatMap { self.defaultRequest(endpoint) }
.mapError { $0 as Error }
.eraseToAnyPublisher()
} else {
return defaultRequest(endpoint)
.retry(maxRetryCount)
.eraseToAnyPublisher()
}
}

func checkTokenIsExpired() -> Bool {
let expired = jwtStore.load(property: .accessExpiresAt)
.toDateWithCustomFormat("yyyy-MM-dd'T'HH:mm:ss")
return Date() > expired
}

func checkIsEndpointNeedsAuthorization(_ endpoint: Endpoint) -> Bool {
endpoint.jwtTokenType == .accessToken
}

func reissueToken() -> AnyPublisher<Void, Error> {
let client = EmdpointClient<RefreshEndpoint>(
interceptors: [JwtInterceptor(jwtStore: jwtStore)]
)
return client.requestPublisher(.refresh)
.map { _ in }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
}
6 changes: 3 additions & 3 deletions Projects/Domain/BaseDomain/Sources/DotoriEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ extension DotoriEndpoint {
) ?? URL(string: "https://www.google.com")!
}

public var validationCode: ClosedRange<Int> {
200...300
}
public var validationCode: ClosedRange<Int> { 200...300 }

public var headers: [String: String]? {
switch self {
Expand All @@ -24,6 +22,8 @@ extension DotoriEndpoint {
return ["Content-Type": "application/json"]
}
}

public var timeout: TimeInterval { 60 }
}

private class BundleFinder {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import DateUtility
import Emdpoint
import Foundation
import JwtStoreInterface
Expand All @@ -12,7 +13,7 @@ public struct JwtInterceptor: InterceptorType {
public func prepare(
_ request: URLRequest,
endpoint: EndpointType,
completion: (Result<URLRequest, EmdpointError>) -> Void
completion: @escaping (Result<URLRequest, EmdpointError>) -> Void
) {
guard let jwtTokenType = (endpoint as? JwtAuthorizable)?.jwtTokenType,
jwtTokenType != .none
Expand All @@ -22,8 +23,12 @@ public struct JwtInterceptor: InterceptorType {
}
var req = request
let token = getToken(jwtTokenType: jwtTokenType)
req.addValue(jwtTokenType.rawValue, forHTTPHeaderField: token)
completion(.success(request))
req.setValue(token, forHTTPHeaderField: jwtTokenType.rawValue)
if checkTokenIsExpired() {
reissueToken(req, jwtType: jwtTokenType, completion: completion)
} else {
completion(.success(request))
}
}

public func didReceive(
Expand Down Expand Up @@ -61,4 +66,36 @@ private extension JwtInterceptor {
jwtStore.save(property: .refreshToken, value: tokenDTO.refreshToken)
jwtStore.save(property: .accessExpiresAt, value: tokenDTO.expiresAt)
}

func checkTokenIsExpired() -> Bool {
let expired = jwtStore.load(property: .accessExpiresAt)
.toDateWithCustomFormat("yyyy-MM-dd'T'HH:mm:ss")
return Date() > expired
}

func reissueToken(
_ request: URLRequest,
jwtType: JwtTokenType,
completion: @escaping (Result<URLRequest, EmdpointError>) -> Void
) {
#if DEV || STAGE
let client = EmdpointClient<RefreshEndpoint>(interceptors: [DotoriLoggingInterceptor()])
#else
let client = EmdpointClient<RefreshEndpoint>()
#endif
client.request(.refresh) { result in
switch result {
case let .success(response):
var request = request
if let tokenDTO = try? JSONDecoder().decode(JwtTokenDTO.self, from: response.data) {
saveToken(tokenDTO: tokenDTO)
request.setValue(getToken(jwtTokenType: jwtType), forHTTPHeaderField: jwtType.rawValue)
}
completion(.success(request))

case let .failure(error):
completion(.failure(error))
}
}
}
}
2 changes: 1 addition & 1 deletion Tuist/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let dependencies = Dependencies(
swiftPackageManager: SwiftPackageManagerDependencies(
[
.remote(url: "https://github.com/GSM-MSG/Moordinator.git", requirement: .exact("1.1.1")),
.remote(url: "https://github.com/GSM-MSG/Emdpoint.git", requirement: .exact("3.2.2")),
.remote(url: "https://github.com/GSM-MSG/Emdpoint.git", requirement: .exact("3.2.4")),
.remote(url: "https://github.com/GSM-MSG/MSGLayout.git", requirement: .exact("1.1.0")),
.remote(url: "https://github.com/Swinject/Swinject.git", requirement: .exact("2.8.3")),

Expand Down