mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
2FA seems to work now
This commit is contained in:
parent
a081a1392e
commit
ad267e2b56
11 changed files with 457 additions and 343 deletions
|
|
@ -2,6 +2,6 @@
|
|||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:/Users/brandon/Projects/XcodesApp/Xcodes.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class AppState: ObservableObject {
|
|||
case installing(Progress)
|
||||
case installed
|
||||
}
|
||||
|
||||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||
@Published var allVersions: [XcodeVersion] = []
|
||||
|
||||
struct AlertContent: Identifiable {
|
||||
|
|
@ -32,53 +34,184 @@ class AppState: ObservableObject {
|
|||
@Published var error: AlertContent?
|
||||
|
||||
@Published var presentingSignInAlert = false
|
||||
@Published var secondFactorSessionData: AppleSessionData?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
let client = AppleAPI.Client()
|
||||
|
||||
func load() {
|
||||
// if list.shouldUpdate {
|
||||
update()
|
||||
.done { _ in
|
||||
self.updateAllVersions()
|
||||
}
|
||||
.catch { error in
|
||||
self.error = AlertContent(title: "Error",
|
||||
message: error.localizedDescription)
|
||||
}
|
||||
// }
|
||||
// .done { _ in
|
||||
// self.updateAllVersions()
|
||||
// }
|
||||
// .catch { error in
|
||||
// self.error = AlertContent(title: "Error",
|
||||
// message: error.localizedDescription)
|
||||
// }
|
||||
//// }
|
||||
// else {
|
||||
// updateAllVersions()
|
||||
// }
|
||||
}
|
||||
|
||||
func validateSession() -> Promise<Void> {
|
||||
return firstly { () -> Promise<Void> in
|
||||
return Current.network.validateSession()
|
||||
}
|
||||
.recover { _ in
|
||||
self.presentingSignInAlert = true
|
||||
func validateSession() -> AnyPublisher<Void, Error> {
|
||||
return client.validateSession()
|
||||
.handleEvents(receiveCompletion: { completion in
|
||||
if case .failure = completion {
|
||||
self.authenticationState = .unauthenticated
|
||||
self.presentingSignInAlert = true
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func login(username: String, password: String) {
|
||||
client.login(accountName: username, password: password)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
if case .failure = completion {
|
||||
// TODO: show error
|
||||
}
|
||||
},
|
||||
receiveValue: { authenticationState in
|
||||
self.authenticationState = authenticationState
|
||||
if case let AuthenticationState.waitingForSecondFactor(option, sessionData) = authenticationState {
|
||||
self.handleTwoFactorOption(option, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
|
||||
}
|
||||
}
|
||||
)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func handleTwoFactorOption(_ option: TwoFactorOption, serviceKey: String, sessionID: String, scnt: String) {
|
||||
// Current.logging.log("Two-factor authentication is enabled for this account.\n")
|
||||
switch option {
|
||||
case let .smsSent(codeLength, phoneNumber):
|
||||
break
|
||||
// return Result {
|
||||
// let code = self.promptForSMSSecurityCode(length: codeLength, for: phoneNumber)
|
||||
// return try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code)
|
||||
// }
|
||||
// .publisher
|
||||
// .flatMap { request in
|
||||
// return Current.network.dataTask(with: request)
|
||||
// .validateSecurityCodeResponse()
|
||||
// .mapError { $0 as Error }
|
||||
// }
|
||||
// .flatMap { (data, response) in
|
||||
// self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
case let .smsPendingChoice(codeLength, trustedPhoneNumbers):
|
||||
break
|
||||
// return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: trustedPhoneNumbers, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
case let .codeSent(codeLength):
|
||||
self.presentingSignInAlert = false
|
||||
self.secondFactorSessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
// let code = Current.shell.readLine("""
|
||||
// Enter "sms" without quotes to exit this prompt and choose a phone number to send an SMS security code to.
|
||||
// Enter the \(codeLength) digit code from one of your trusted devices:
|
||||
// """) ?? ""
|
||||
//
|
||||
// if code == "sms" {
|
||||
// return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: authOp, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func continueLogin(username: String, password: String) -> Promise<Void> {
|
||||
firstly { () -> Promise<Void> in
|
||||
self.installer.login(username, password: password)
|
||||
}
|
||||
.recover { error -> Promise<Void> in
|
||||
XcodesKit.Current.logging.log(error.legibleLocalizedDescription)
|
||||
|
||||
if case Client.Error.invalidUsernameOrPassword = error {
|
||||
self.presentingSignInAlert = true
|
||||
}
|
||||
return Promise(error: error)
|
||||
}
|
||||
// func selectPhoneNumberInteractively(from trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]) -> AnyPublisher<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error> {
|
||||
// return Result {
|
||||
// Current.logging.log("Trusted phone numbers:")
|
||||
// trustedPhoneNumbers.enumerated().forEach { (index, phoneNumber) in
|
||||
// Current.logging.log("\(index + 1): \(phoneNumber.numberWithDialCode)")
|
||||
// }
|
||||
//
|
||||
// let possibleSelectionNumberString = Current.shell.readLine("Select a trusted phone number to receive a code via SMS: ")
|
||||
// guard
|
||||
// let selectionNumberString = possibleSelectionNumberString,
|
||||
// let selectionNumber = Int(selectionNumberString) ,
|
||||
// trustedPhoneNumbers.indices.contains(selectionNumber - 1)
|
||||
// else {
|
||||
// throw AuthenticationError.invalidPhoneNumberIndex(min: 1, max: trustedPhoneNumbers.count, given: possibleSelectionNumberString)
|
||||
// }
|
||||
//
|
||||
// return trustedPhoneNumbers[selectionNumber - 1]
|
||||
// }
|
||||
// .publisher
|
||||
// .catch { error -> AnyPublisher<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error> in
|
||||
// guard case AuthenticationError.invalidPhoneNumberIndex = error else {
|
||||
// return Fail<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error>(error: error).eraseToAnyPublisher()
|
||||
// }
|
||||
// Current.logging.log("\(error.localizedDescription)\n")
|
||||
// return self.selectPhoneNumberInteractively(from: trustedPhoneNumbers)
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
// }
|
||||
//
|
||||
// func promptForSMSSecurityCode(length: Int, for trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber) -> SecurityCode {
|
||||
// let code = Current.shell.readLine("Enter the \(length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ") ?? ""
|
||||
// return .sms(code: code, phoneNumberId: trustedPhoneNumber.id)
|
||||
// }
|
||||
|
||||
// func handleWithPhoneNumberSelection(codeLength: Int, trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?, serviceKey: String, sessionID: String, scnt: String) -> AnyPublisher<AuthenticationState, Error> {
|
||||
// // I don't think this should ever be nil or empty, because 2FA requires at least one trusted phone number,
|
||||
// // but if it is nil or empty it's better to inform the user so they can try to address it instead of crashing.
|
||||
// guard let trustedPhoneNumbers = trustedPhoneNumbers, trustedPhoneNumbers.isEmpty == false else {
|
||||
// return Fail(error: AuthenticationError.noTrustedPhoneNumbers)
|
||||
// .eraseToAnyPublisher()
|
||||
// }
|
||||
//
|
||||
// return selectPhoneNumberInteractively(from: trustedPhoneNumbers)
|
||||
// .flatMap { trustedPhoneNumber in
|
||||
// Current.network.dataTask(with: try URLRequest.requestSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, trustedPhoneID: trustedPhoneNumber.id))
|
||||
// .map { _ in
|
||||
// self.promptForSMSSecurityCode(length: codeLength, for: trustedPhoneNumber)
|
||||
// }
|
||||
// }
|
||||
// .flatMap { code in
|
||||
// Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code))
|
||||
// .validateSecurityCodeResponse()
|
||||
// }
|
||||
// .flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
|
||||
// self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
// }
|
||||
|
||||
func submit2FACode(_ code: String, sessionData: AppleSessionData) {
|
||||
client.submitSecurityCode(code, sessionData: sessionData)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
self.error = AlertContent(title: "Error logging in", message: error.legibleLocalizedDescription)
|
||||
case .finished:
|
||||
if case .authenticated = self.authenticationState {
|
||||
self.presentingSignInAlert = false
|
||||
self.secondFactorSessionData = nil
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveValue: { authenticationState in
|
||||
self.authenticationState = authenticationState
|
||||
}
|
||||
)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
public func update() -> Promise<[Xcode]> {
|
||||
return firstly { () -> Promise<Void> in
|
||||
validateSession()
|
||||
}
|
||||
.then { () -> Promise<[Xcode]> in
|
||||
self.list.update()
|
||||
}
|
||||
public func update() -> AnyPublisher<[Xcode], Error> {
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// validateSession()
|
||||
// }
|
||||
// .then { () -> Promise<[Xcode]> in
|
||||
// self.list.update()
|
||||
// }
|
||||
Just<[Xcode]>([])
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func updateAllVersions() {
|
||||
|
|
|
|||
|
|
@ -5,24 +5,20 @@ import PackageDescription
|
|||
|
||||
let package = Package(
|
||||
name: "AppleAPI",
|
||||
platforms: [.macOS(.v10_13)],
|
||||
platforms: [.macOS(.v10_15)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "AppleAPI",
|
||||
targets: ["AppleAPI"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
.package(url: "https://github.com/mxcl/PromiseKit.git", .upToNextMajor(from: "6.8.3")),
|
||||
.package(name: "PMKFoundation", url: "https://github.com/PromiseKit/Foundation.git", .upToNextMajor(from: "3.3.1")),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "AppleAPI",
|
||||
dependencies: ["PromiseKit", "PMKFoundation"]),
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "AppleAPITests",
|
||||
dependencies: ["AppleAPI"]),
|
||||
|
|
|
|||
|
|
@ -1,123 +1,84 @@
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
import PMKFoundation
|
||||
import Combine
|
||||
|
||||
public class Client {
|
||||
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
|
||||
|
||||
public init() {}
|
||||
|
||||
public enum Error: Swift.Error, LocalizedError, Equatable {
|
||||
case invalidSession
|
||||
case invalidUsernameOrPassword(username: String)
|
||||
case invalidPhoneNumberIndex(min: Int, max: Int, given: String?)
|
||||
case incorrectSecurityCode
|
||||
case unexpectedSignInResponse(statusCode: Int, message: String?)
|
||||
case appleIDAndPrivacyAcknowledgementRequired
|
||||
case noTrustedPhoneNumbers
|
||||
// MARK: - Login
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidUsernameOrPassword(let username):
|
||||
return "Invalid username and password combination. Attempted to sign in with username \(username)."
|
||||
case .appleIDAndPrivacyAcknowledgementRequired:
|
||||
return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement."
|
||||
case .invalidPhoneNumberIndex(let min, let max, let given):
|
||||
return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")."
|
||||
case .noTrustedPhoneNumbers:
|
||||
return "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915."
|
||||
default:
|
||||
return String(describing: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the olympus session endpoint to see if the existing session is still valid
|
||||
public func validateSession() -> Promise<Void> {
|
||||
return Current.network.dataTask(with: URLRequest.olympusSession)
|
||||
.done { data, response in
|
||||
guard
|
||||
let jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
||||
jsonObject["provider"] != nil
|
||||
else { throw Error.invalidSession }
|
||||
}
|
||||
}
|
||||
|
||||
public func login(accountName: String, password: String) -> Promise<Void> {
|
||||
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||
var serviceKey: String!
|
||||
|
||||
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
|
||||
Current.network.dataTask(with: URLRequest.itcServiceKey)
|
||||
}
|
||||
.then { (data, _) -> Promise<(data: Data, response: URLResponse)> in
|
||||
struct ServiceKeyResponse: Decodable {
|
||||
let authServiceKey: String
|
||||
return Current.network.dataTask(with: URLRequest.itcServiceKey)
|
||||
.map(\.data)
|
||||
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
|
||||
.flatMap { serviceKeyResponse -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
||||
serviceKey = serviceKeyResponse.authServiceKey
|
||||
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password))
|
||||
.mapError { $0 as Swift.Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data)
|
||||
serviceKey = response.authServiceKey
|
||||
|
||||
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password))
|
||||
}
|
||||
.then { (data, response) -> Promise<Void> in
|
||||
struct SignInResponse: Decodable {
|
||||
let authType: String?
|
||||
let serviceErrors: [ServiceError]?
|
||||
|
||||
struct ServiceError: Decodable, CustomStringConvertible {
|
||||
let code: String
|
||||
let message: String
|
||||
|
||||
var description: String {
|
||||
return "\(code): \(message)"
|
||||
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
|
||||
let (data, response) = result
|
||||
return Just(data)
|
||||
.decode(type: SignInResponse.self, decoder: JSONDecoder())
|
||||
.flatMap { responseBody -> AnyPublisher<AuthenticationState, Swift.Error> in
|
||||
let httpResponse = response as! HTTPURLResponse
|
||||
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
return Current.network.dataTask(with: URLRequest.olympusSession)
|
||||
.map { _ in AuthenticationState.authenticated }
|
||||
.mapError { $0 as Swift.Error }
|
||||
.eraseToAnyPublisher()
|
||||
case 401:
|
||||
return Fail(error: AuthenticationError.invalidUsernameOrPassword(username: accountName))
|
||||
.eraseToAnyPublisher()
|
||||
case 409:
|
||||
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
|
||||
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
|
||||
return Fail(error: AuthenticationError.appleIDAndPrivacyAcknowledgementRequired)
|
||||
.eraseToAnyPublisher()
|
||||
default:
|
||||
return Fail(error: AuthenticationError.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
|
||||
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", ")))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let httpResponse = response as! HTTPURLResponse
|
||||
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
|
||||
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
|
||||
case 401:
|
||||
throw Error.invalidUsernameOrPassword(username: accountName)
|
||||
case 409:
|
||||
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
|
||||
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
|
||||
throw Error.appleIDAndPrivacyAcknowledgementRequired
|
||||
default:
|
||||
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
|
||||
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
|
||||
}
|
||||
}
|
||||
.mapError { $0 as Swift.Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> Promise<Void> {
|
||||
func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||
let httpResponse = response as! HTTPURLResponse
|
||||
let sessionID = (httpResponse.allHeaderFields["X-Apple-ID-Session-Id"] as! String)
|
||||
let scnt = (httpResponse.allHeaderFields["scnt"] as! String)
|
||||
|
||||
return firstly { () -> Promise<AuthOptionsResponse> in
|
||||
return Current.network.dataTask(with: URLRequest.authOptions(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
|
||||
.map { try JSONDecoder().decode(AuthOptionsResponse.self, from: $0.data) }
|
||||
}
|
||||
.then { authOptions -> Promise<Void> in
|
||||
switch authOptions.kind {
|
||||
case .twoStep:
|
||||
Current.logging.log("Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or open an issue on GitHub explaining why this isn't an option for you here: https://github.com/RobotsAndPencils/xcodes/issues/new")
|
||||
return Promise.value(())
|
||||
case .twoFactor:
|
||||
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
||||
case .unknown:
|
||||
Current.logging.log("Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response:")
|
||||
String(data: data, encoding: .utf8).map { Current.logging.log($0) }
|
||||
return Promise.value(())
|
||||
return Current.network.dataTask(with: URLRequest.authOptions(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
|
||||
.map(\.data)
|
||||
.decode(type: AuthOptionsResponse.self, decoder: JSONDecoder())
|
||||
.flatMap { authOptions -> AnyPublisher<AuthenticationState, Error> in
|
||||
switch authOptions.kind {
|
||||
case .twoStep:
|
||||
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
||||
.eraseToAnyPublisher()
|
||||
case .twoFactor:
|
||||
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
||||
.eraseToAnyPublisher()
|
||||
case .unknown:
|
||||
let possibleResponseString = String(data: data, encoding: .utf8)
|
||||
return Fail(error: AuthenticationError.accountUsesUnknownAuthenticationKind(possibleResponseString))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func handleTwoFactor(serviceKey: String, sessionID: String, scnt: String, authOptions: AuthOptionsResponse) -> Promise<Void> {
|
||||
func handleTwoFactor(serviceKey: String, sessionID: String, scnt: String, authOptions: AuthOptionsResponse) -> AnyPublisher<AuthenticationState, Error> {
|
||||
let option: TwoFactorOption
|
||||
|
||||
// SMS was sent automatically
|
||||
|
|
@ -131,132 +92,153 @@ public class Client {
|
|||
option = .codeSent(authOptions.securityCode.length)
|
||||
}
|
||||
|
||||
return handleTwoFactorOption(option, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
return Just(AuthenticationState.waitingForSecondFactor(option, sessionData))
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
// return handleTwoFactorOption(option, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
}
|
||||
|
||||
func handleTwoFactorOption(_ option: TwoFactorOption, serviceKey: String, sessionID: String, scnt: String) -> Promise<Void> {
|
||||
Current.logging.log("Two-factor authentication is enabled for this account.\n")
|
||||
switch option {
|
||||
case let .smsSent(codeLength, phoneNumber):
|
||||
return firstly { () throws -> Promise<(data: Data, response: URLResponse)> in
|
||||
let code = self.promptForSMSSecurityCode(length: codeLength, for: phoneNumber)
|
||||
return Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code))
|
||||
.validateSecurityCodeResponse()
|
||||
}
|
||||
.then { (data, response) -> Promise<Void> in
|
||||
self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
}
|
||||
case let .smsPendingChoice(codeLength, trustedPhoneNumbers):
|
||||
return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: trustedPhoneNumbers, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
case let .codeSent(codeLength):
|
||||
let code = Current.shell.readLine("""
|
||||
Enter "sms" without quotes to exit this prompt and choose a phone number to send an SMS security code to.
|
||||
Enter the \(codeLength) digit code from one of your trusted devices:
|
||||
""") ?? ""
|
||||
|
||||
if code == "sms" {
|
||||
// return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: authOp, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
}
|
||||
|
||||
return firstly {
|
||||
Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: .device(code: code)))
|
||||
.validateSecurityCodeResponse()
|
||||
|
||||
}
|
||||
.then { (data, response) -> Promise<Void> in
|
||||
self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
}
|
||||
// MARK: - Continue 2FA
|
||||
|
||||
public func submitSecurityCode(_ code: String, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
|
||||
Result {
|
||||
try URLRequest.submitSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, code: .device(code: code))
|
||||
}
|
||||
}
|
||||
|
||||
func updateSession(serviceKey: String, sessionID: String, scnt: String) -> Promise<Void> {
|
||||
return Current.network.dataTask(with: URLRequest.trust(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
|
||||
.then { (data, response) -> Promise<Void> in
|
||||
Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
|
||||
}
|
||||
}
|
||||
|
||||
func selectPhoneNumberInteractively(from trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]) -> Promise<AuthOptionsResponse.TrustedPhoneNumber> {
|
||||
return firstly { () throws -> Guarantee<AuthOptionsResponse.TrustedPhoneNumber> in
|
||||
Current.logging.log("Trusted phone numbers:")
|
||||
trustedPhoneNumbers.enumerated().forEach { (index, phoneNumber) in
|
||||
Current.logging.log("\(index + 1): \(phoneNumber.numberWithDialCode)")
|
||||
}
|
||||
|
||||
let possibleSelectionNumberString = Current.shell.readLine("Select a trusted phone number to receive a code via SMS: ")
|
||||
guard
|
||||
let selectionNumberString = possibleSelectionNumberString,
|
||||
let selectionNumber = Int(selectionNumberString) ,
|
||||
trustedPhoneNumbers.indices.contains(selectionNumber - 1)
|
||||
else {
|
||||
throw Error.invalidPhoneNumberIndex(min: 1, max: trustedPhoneNumbers.count, given: possibleSelectionNumberString)
|
||||
}
|
||||
|
||||
return .value(trustedPhoneNumbers[selectionNumber - 1])
|
||||
}
|
||||
.recover { error throws -> Promise<AuthOptionsResponse.TrustedPhoneNumber> in
|
||||
guard case Error.invalidPhoneNumberIndex = error else { throw error }
|
||||
Current.logging.log("\(error.localizedDescription)\n")
|
||||
return self.selectPhoneNumberInteractively(from: trustedPhoneNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
func promptForSMSSecurityCode(length: Int, for trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber) -> SecurityCode {
|
||||
let code = Current.shell.readLine("Enter the \(length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ") ?? ""
|
||||
return .sms(code: code, phoneNumberId: trustedPhoneNumber.id)
|
||||
}
|
||||
|
||||
func handleWithPhoneNumberSelection(codeLength: Int, trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?, serviceKey: String, sessionID: String, scnt: String) -> Promise<Void> {
|
||||
return firstly { () throws -> Promise<AuthOptionsResponse.TrustedPhoneNumber> in
|
||||
// I don't think this should ever be nil or empty, because 2FA requires at least one trusted phone number,
|
||||
// but if it is nil or empty it's better to inform the user so they can try to address it instead of crashing.
|
||||
guard let trustedPhoneNumbers = trustedPhoneNumbers, trustedPhoneNumbers.isEmpty == false else {
|
||||
throw Error.noTrustedPhoneNumbers
|
||||
}
|
||||
|
||||
return selectPhoneNumberInteractively(from: trustedPhoneNumbers)
|
||||
}
|
||||
.then { trustedPhoneNumber in
|
||||
Current.network.dataTask(with: try URLRequest.requestSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, trustedPhoneID: trustedPhoneNumber.id))
|
||||
.map { _ in
|
||||
self.promptForSMSSecurityCode(length: codeLength, for: trustedPhoneNumber)
|
||||
.publisher
|
||||
.flatMap { request in
|
||||
Current.network.dataTask(with: request)
|
||||
.mapError { $0 as Error }
|
||||
// .validateSecurityCodeResponse()
|
||||
.flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
|
||||
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
|
||||
}
|
||||
}
|
||||
.then { code in
|
||||
Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code))
|
||||
.validateSecurityCodeResponse()
|
||||
}
|
||||
.then { (data, response) -> Promise<Void> in
|
||||
self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Session
|
||||
|
||||
/// Use the olympus session endpoint to see if the existing session is still valid
|
||||
public func validateSession() -> AnyPublisher<Void, Error> {
|
||||
return Current.network.dataTask(with: URLRequest.olympusSession)
|
||||
.tryMap { (data, response) in
|
||||
guard
|
||||
let jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
||||
jsonObject["provider"] != nil
|
||||
else { throw AuthenticationError.invalidSession }
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateSession(serviceKey: String, sessionID: String, scnt: String) -> AnyPublisher<AuthenticationState, Error> {
|
||||
return Current.network.dataTask(with: URLRequest.trust(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
|
||||
.flatMap { (data, response) in
|
||||
Current.network.dataTask(with: URLRequest.olympusSession)
|
||||
.map { _ in AuthenticationState.authenticated }
|
||||
}
|
||||
.mapError { $0 as Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
public enum AuthenticationState: Equatable {
|
||||
case unauthenticated
|
||||
case waitingForSecondFactor(TwoFactorOption, AppleSessionData)
|
||||
case authenticated
|
||||
}
|
||||
|
||||
public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
||||
case invalidSession
|
||||
case invalidUsernameOrPassword(username: String)
|
||||
case invalidPhoneNumberIndex(min: Int, max: Int, given: String?)
|
||||
case incorrectSecurityCode
|
||||
case unexpectedSignInResponse(statusCode: Int, message: String?)
|
||||
case appleIDAndPrivacyAcknowledgementRequired
|
||||
case noTrustedPhoneNumbers
|
||||
case accountUsesTwoStepAuthentication
|
||||
case accountUsesUnknownAuthenticationKind(String?)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidUsernameOrPassword(let username):
|
||||
return "Invalid username and password combination. Attempted to sign in with username \(username)."
|
||||
case .appleIDAndPrivacyAcknowledgementRequired:
|
||||
return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement."
|
||||
case .invalidPhoneNumberIndex(let min, let max, let given):
|
||||
return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")."
|
||||
case .noTrustedPhoneNumbers:
|
||||
return "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915."
|
||||
case .accountUsesTwoStepAuthentication:
|
||||
return "Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or open an issue on GitHub explaining why this isn't an option for you here: https://github.com/RobotsAndPencils/xcodes/issues/new"
|
||||
case .accountUsesUnknownAuthenticationKind:
|
||||
return "Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response:"
|
||||
default:
|
||||
return String(describing: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TwoFactorOption {
|
||||
public struct AppleSessionData: Equatable, Identifiable {
|
||||
public let serviceKey: String
|
||||
public let sessionID: String
|
||||
public let scnt: String
|
||||
|
||||
public var id: String { sessionID }
|
||||
|
||||
public init(serviceKey: String, sessionID: String, scnt: String) {
|
||||
self.serviceKey = serviceKey
|
||||
self.sessionID = sessionID
|
||||
self.scnt = scnt
|
||||
}
|
||||
}
|
||||
|
||||
struct ServiceKeyResponse: Decodable {
|
||||
let authServiceKey: String
|
||||
}
|
||||
|
||||
struct SignInResponse: Decodable {
|
||||
let authType: String?
|
||||
let serviceErrors: [ServiceError]?
|
||||
|
||||
struct ServiceError: Decodable, CustomStringConvertible {
|
||||
let code: String
|
||||
let message: String
|
||||
|
||||
var description: String {
|
||||
return "\(code): \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TwoFactorOption: Equatable {
|
||||
case smsSent(Int, AuthOptionsResponse.TrustedPhoneNumber)
|
||||
case codeSent(Int)
|
||||
case smsPendingChoice(Int, [AuthOptionsResponse.TrustedPhoneNumber])
|
||||
}
|
||||
|
||||
public extension Promise where T == (data: Data, response: URLResponse) {
|
||||
func validateSecurityCodeResponse() -> Promise<T> {
|
||||
validate()
|
||||
.recover { error -> Promise<(data: Data, response: URLResponse)> in
|
||||
switch error {
|
||||
case PMKHTTPError.badStatusCode(let code, _, _):
|
||||
if code == 401 {
|
||||
throw Client.Error.incorrectSecurityCode
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
default:
|
||||
throw error
|
||||
}
|
||||
}
|
||||
public extension Publisher where Output == (data: Data, response: URLResponse) {
|
||||
func validateSecurityCodeResponse() -> AnyPublisher<Output, Failure> {
|
||||
self.eraseToAnyPublisher()
|
||||
// validate()
|
||||
// .recover { error -> AnyPublisher<Output, Swift.Error> in
|
||||
// switch error {
|
||||
// case PMKHTTPError.badStatusCode(let code, _, _):
|
||||
// if code == 401 {
|
||||
// throw Client.Error.incorrectSecurityCode
|
||||
// } else {
|
||||
// throw error
|
||||
// }
|
||||
// default:
|
||||
// throw error
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthOptionsResponse: Decodable {
|
||||
public struct AuthOptionsResponse: Equatable, Decodable {
|
||||
let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
||||
let trustedDevices: [TrustedDevice]?
|
||||
let securityCode: SecurityCodeInfo
|
||||
|
|
@ -285,18 +267,18 @@ struct AuthOptionsResponse: Decodable {
|
|||
trustedPhoneNumbers?.count == 1 && canFallBackToSMS
|
||||
}
|
||||
|
||||
struct TrustedPhoneNumber: Decodable {
|
||||
public struct TrustedPhoneNumber: Equatable, Decodable {
|
||||
let id: Int
|
||||
let numberWithDialCode: String
|
||||
}
|
||||
|
||||
struct TrustedDevice: Decodable {
|
||||
public struct TrustedDevice: Equatable, Decodable {
|
||||
let id: String
|
||||
let name: String
|
||||
let modelName: String
|
||||
}
|
||||
|
||||
struct SecurityCodeInfo: Decodable {
|
||||
public struct SecurityCodeInfo: Equatable, Decodable {
|
||||
let length: Int
|
||||
let tooManyCodesSent: Bool
|
||||
let tooManyCodesValidated: Bool
|
||||
|
|
@ -304,7 +286,7 @@ struct AuthOptionsResponse: Decodable {
|
|||
let securityCodeCooldown: Bool
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
public enum Kind: Equatable {
|
||||
case twoStep, twoFactor, unknown
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
import PMKFoundation
|
||||
import Combine
|
||||
|
||||
/**
|
||||
Lightweight dependency injection using global mutable state :P
|
||||
|
|
@ -10,29 +9,18 @@ import PMKFoundation
|
|||
- SeeAlso: https://vimeo.com/291588126
|
||||
*/
|
||||
public struct Environment {
|
||||
public var shell = Shell()
|
||||
public var network = Network()
|
||||
public var logging = Logging()
|
||||
}
|
||||
|
||||
public var Current = Environment()
|
||||
|
||||
public struct Shell {
|
||||
public var readLine: (String) -> String? = { prompt in
|
||||
print(prompt, terminator: "")
|
||||
return Swift.readLine()
|
||||
}
|
||||
public func readLine(prompt: String) -> String? {
|
||||
readLine(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Network {
|
||||
public var session = URLSession.shared
|
||||
|
||||
public var dataTask: (URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> = { Current.network.session.dataTask(.promise, with: $0) }
|
||||
public func dataTask(with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> {
|
||||
dataTask(convertible)
|
||||
public var dataTask: (URLRequest) -> URLSession.DataTaskPublisher = { Current.network.session.dataTaskPublisher(for: $0) }
|
||||
public func dataTask(with request: URLRequest) -> URLSession.DataTaskPublisher {
|
||||
dataTask(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
Button("Login", action: { self.appState.presentingSignInAlert = true })
|
||||
.sheet(isPresented: $appState.presentingSignInAlert) {
|
||||
SignInCredentialsView(isPresented: $appState.presentingSignInAlert)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
Button(action: { self.appState.update() }) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
|
|
@ -102,10 +107,11 @@ struct ContentView: View {
|
|||
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: row.id) }),
|
||||
secondaryButton: .cancel(Text("Cancel")))
|
||||
}
|
||||
.sheet(isPresented: $appState.presentingSignInAlert) {
|
||||
SignInCredentialsView(isPresented: $appState.presentingSignInAlert)
|
||||
.sheet(item: $appState.secondFactorSessionData) { sessionData in
|
||||
SignIn2FAView(isPresented: $appState.secondFactorSessionData.isNotNil, sessionData: sessionData)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,3 +133,11 @@ struct ContentView_Previews: PreviewProvider {
|
|||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional {
|
||||
/// Note that this is lossy when setting, so you can really only set it to nil, but this is sufficient for mapping `Binding<Item?>` to `Binding<Bool>` for Alerts, Popovers, etc.
|
||||
var isNotNil: Bool {
|
||||
get { self != nil }
|
||||
set { self = newValue ? self : nil }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ struct SignIn2FAView: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@Binding var isPresented: Bool
|
||||
@State private var code: String = ""
|
||||
let sessionData: AppleSessionData
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -18,7 +19,7 @@ struct SignIn2FAView: View {
|
|||
Button("Cancel", action: { isPresented = false })
|
||||
Button("Send SMS", action: {})
|
||||
Spacer()
|
||||
Button("Continue", action: {})
|
||||
Button("Continue", action: { appState.submit2FACode(code, sessionData: sessionData) })
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
|
@ -27,7 +28,7 @@ struct SignIn2FAView: View {
|
|||
|
||||
struct SignIn2FAView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SignIn2FAView(isPresented: .constant(true))
|
||||
SignIn2FAView(isPresented: .constant(true), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: ""))
|
||||
.environmentObject(AppState())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ struct SignInCredentialsView: View {
|
|||
Spacer()
|
||||
Button("Cancel") { isPresented = false }
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Button("Next") { appState.continueLogin(username: username, password: password) }
|
||||
Button("Next") { appState.login(username: username, password: password) }
|
||||
.disabled(username.isEmpty)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import PackageDescription
|
|||
|
||||
let package = Package(
|
||||
name: "XcodesKit",
|
||||
platforms: [.macOS(.v10_13)],
|
||||
platforms: [.macOS(.v10_15)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
|
|
|||
|
|
@ -246,12 +246,12 @@ public struct Network {
|
|||
return downloadTask(convertible, saveLocation, resumeData)
|
||||
}
|
||||
|
||||
public var validateSession: () -> Promise<Void> = client.validateSession
|
||||
|
||||
public var login: (String, String) -> Promise<Void> = { client.login(accountName: $0, password: $1) }
|
||||
public func login(accountName: String, password: String) -> Promise<Void> {
|
||||
login(accountName, password)
|
||||
}
|
||||
// public var validateSession: () -> Promise<Void> = client.validateSession
|
||||
//
|
||||
// public var login: (String, String) -> Promise<Void> = { client.login(accountName: $0, password: $1) }
|
||||
// public func login(accountName: String, password: String) -> Promise<Void> {
|
||||
// login(accountName, password)
|
||||
// }
|
||||
}
|
||||
|
||||
public struct Logging {
|
||||
|
|
|
|||
|
|
@ -248,9 +248,9 @@ public final class XcodeInstaller {
|
|||
|
||||
private func downloadXcode(version: Version, downloader: Downloader) -> Promise<(Xcode, URL)> {
|
||||
return firstly { () -> Promise<Version> in
|
||||
loginIfNeeded().map { version }
|
||||
}
|
||||
.then { version -> Promise<Version> in
|
||||
// loginIfNeeded().map { version }
|
||||
// }
|
||||
// .then { version -> Promise<Version> in
|
||||
if self.xcodeList.shouldUpdate {
|
||||
return self.xcodeList.update().map { _ in version }
|
||||
}
|
||||
|
|
@ -282,60 +282,60 @@ public final class XcodeInstaller {
|
|||
}
|
||||
}
|
||||
|
||||
func loginIfNeeded(withUsername existingUsername: String? = nil) -> Promise<Void> {
|
||||
return firstly { () -> Promise<Void> in
|
||||
return Current.network.validateSession()
|
||||
}
|
||||
.recover { error -> Promise<Void> in
|
||||
guard
|
||||
let username = existingUsername ?? self.findUsername() ?? Current.shell.readLine(prompt: "Apple ID: "),
|
||||
let password = self.findPassword(withUsername: username) ?? Current.shell.readSecureLine(prompt: "Apple ID Password: ")
|
||||
else { throw Error.missingUsernameOrPassword }
|
||||
|
||||
return firstly { () -> Promise<Void> in
|
||||
self.login(username, password: password)
|
||||
}
|
||||
.recover { error -> Promise<Void> in
|
||||
Current.logging.log(error.legibleLocalizedDescription)
|
||||
|
||||
if case Client.Error.invalidUsernameOrPassword = error {
|
||||
Current.logging.log("Try entering your password again")
|
||||
return self.loginIfNeeded(withUsername: username)
|
||||
}
|
||||
else {
|
||||
return Promise(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func login(_ username: String, password: String) -> Promise<Void> {
|
||||
return firstly { () -> Promise<Void> in
|
||||
Current.network.login(accountName: username, password: password)
|
||||
}
|
||||
.recover { error -> Promise<Void> in
|
||||
|
||||
if let error = error as? Client.Error {
|
||||
switch error {
|
||||
case .invalidUsernameOrPassword(_):
|
||||
// remove any keychain password if we fail to log with an invalid username or password so it doesn't try again.
|
||||
try? Current.keychain.remove(username)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Promise(error: error)
|
||||
}
|
||||
.done { _ in
|
||||
try? Current.keychain.set(password, key: username)
|
||||
|
||||
if self.configuration.defaultUsername != username {
|
||||
self.configuration.defaultUsername = username
|
||||
try? self.configuration.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
// func loginIfNeeded(withUsername existingUsername: String? = nil) -> Promise<Void> {
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// return Current.network.validateSession()
|
||||
// }
|
||||
// .recover { error -> Promise<Void> in
|
||||
// guard
|
||||
// let username = existingUsername ?? self.findUsername() ?? Current.shell.readLine(prompt: "Apple ID: "),
|
||||
// let password = self.findPassword(withUsername: username) ?? Current.shell.readSecureLine(prompt: "Apple ID Password: ")
|
||||
// else { throw Error.missingUsernameOrPassword }
|
||||
//
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// self.login(username, password: password)
|
||||
// }
|
||||
// .recover { error -> Promise<Void> in
|
||||
// Current.logging.log(error.legibleLocalizedDescription)
|
||||
//
|
||||
// if case Client.AuthenticationErrro.invalidUsernameOrPassword = error {
|
||||
// Current.logging.log("Try entering your password again")
|
||||
// return self.loginIfNeeded(withUsername: username)
|
||||
// }
|
||||
// else {
|
||||
// return Promise(error: error)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func login(_ username: String, password: String) -> Promise<Void> {
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// Current.network.login(accountName: username, password: password)
|
||||
// }
|
||||
// .recover { error -> Promise<Void> in
|
||||
//
|
||||
// if let error = error as? Client.AuthenticationErrro {
|
||||
// switch error {
|
||||
// case .invalidUsernameOrPassword(_):
|
||||
// // remove any keychain password if we fail to log with an invalid username or password so it doesn't try again.
|
||||
// try? Current.keychain.remove(username)
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return Promise(error: error)
|
||||
// }
|
||||
// .done { _ in
|
||||
// try? Current.keychain.set(password, key: username)
|
||||
//
|
||||
// if self.configuration.defaultUsername != username {
|
||||
// self.configuration.defaultUsername = username
|
||||
// try? self.configuration.save()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
let xcodesUsername = "XCODES_USERNAME"
|
||||
let xcodesPassword = "XCODES_PASSWORD"
|
||||
|
|
@ -511,12 +511,12 @@ public final class XcodeInstaller {
|
|||
}
|
||||
|
||||
public func update() -> Promise<[Xcode]> {
|
||||
return firstly { () -> Promise<Void> in
|
||||
loginIfNeeded()
|
||||
}
|
||||
.then { () -> Promise<[Xcode]> in
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// loginIfNeeded()
|
||||
// }
|
||||
// .then { () -> Promise<[Xcode]> in
|
||||
self.xcodeList.update()
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
public func updateAndPrint(destination: Path) -> Promise<Void> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue