refactor how Credentials work
This commit is contained in:
parent
261e2a951a
commit
fc7b6f2c6b
|
@ -113,7 +113,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
let dataFolder: String
|
||||
let database: ArticlesDatabase
|
||||
let delegate: AccountDelegate
|
||||
var delegate: AccountDelegate
|
||||
static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0)
|
||||
|
||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
||||
|
@ -228,6 +228,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
|
||||
delegate.credentials = try? retrieveBasicCredentials()
|
||||
|
||||
pullObjectsFromDisk()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
@ -242,99 +244,32 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
// MARK: - API
|
||||
|
||||
public func storeCredentials(_ credentials: Credentials) throws {
|
||||
|
||||
guard let username = credentials.username, let password = credentials.password, let server = delegate.server else {
|
||||
guard let server = delegate.server else {
|
||||
throw CredentialsError.incompleteCredentials
|
||||
}
|
||||
|
||||
self.username = username
|
||||
|
||||
let passwordData = password.data(using: String.Encoding.utf8)!
|
||||
|
||||
let updateQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server]
|
||||
let attributes: [String: Any] = [kSecValueData as String: passwordData]
|
||||
let status = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return
|
||||
case errSecItemNotFound:
|
||||
break
|
||||
default:
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
guard status == errSecItemNotFound else {
|
||||
return
|
||||
switch credentials {
|
||||
case .basic(let username, _):
|
||||
self.username = username
|
||||
}
|
||||
|
||||
let addQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server,
|
||||
kSecValueData as String: passwordData]
|
||||
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||
if addStatus != errSecSuccess {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
try CredentialsManager.storeCredentials(credentials, server: server)
|
||||
|
||||
}
|
||||
|
||||
public func retrieveCredentials() throws -> Credentials? {
|
||||
|
||||
public func retrieveBasicCredentials() throws -> Credentials? {
|
||||
guard let username = self.username, let server = delegate.server else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
guard status != errSecItemNotFound else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard status == errSecSuccess else {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
guard let existingItem = item as? [String : Any],
|
||||
let passwordData = existingItem[kSecValueData as String] as? Data,
|
||||
let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return BasicCredentials(username: username, password: password)
|
||||
|
||||
return try CredentialsManager.retrieveBasicCredentials(server: server, username: username)
|
||||
}
|
||||
|
||||
public func removeCredentials() throws {
|
||||
|
||||
public func removeBasicCredentials() throws {
|
||||
guard let username = self.username, let server = delegate.server else {
|
||||
return
|
||||
}
|
||||
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
try CredentialsManager.removeBasicCredentials(server: server, username: username)
|
||||
self.username = nil
|
||||
|
||||
}
|
||||
|
||||
public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void) {
|
||||
|
|
|
@ -214,26 +214,26 @@
|
|||
848934EC1F62484F00CEBD24 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
846E77531F6F00E300A165E2 /* AccountManager.swift */,
|
||||
848935101F62486800CEBD24 /* Account.swift */,
|
||||
84AF4EA3222CFDD100F6A800 /* AccountSettings.swift */,
|
||||
841974241F6DDCE4006346C4 /* AccountDelegate.swift */,
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */,
|
||||
846E77531F6F00E300A165E2 /* AccountManager.swift */,
|
||||
84AF4EA3222CFDD100F6A800 /* AccountSettings.swift */,
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
||||
8419740D1F6DD25F006346C4 /* Container.swift */,
|
||||
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
|
||||
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
|
||||
844B297C2106C7EC004020B3 /* Feed.swift */,
|
||||
84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */,
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */,
|
||||
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
|
||||
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
|
||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
||||
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
||||
8419740D1F6DD25F006346C4 /* Container.swift */,
|
||||
8419742B1F6DDE84006346C4 /* LocalAccount */,
|
||||
848935031F62484F00CEBD24 /* AccountTests */,
|
||||
84245C7D1FDDD2580074AFBB /* Feedbin */,
|
||||
8469F80F1F6DC3C10084783E /* Frameworks */,
|
||||
848934FA1F62484F00CEBD24 /* Info.plist */,
|
||||
848935031F62484F00CEBD24 /* AccountTests */,
|
||||
8419742B1F6DDE84006346C4 /* LocalAccount */,
|
||||
848934F71F62484F00CEBD24 /* Products */,
|
||||
D511EEB4202422BB00712EC3 /* xcconfig */,
|
||||
848934FA1F62484F00CEBD24 /* Info.plist */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
usesTabs = 1;
|
||||
|
|
|
@ -14,8 +14,7 @@ public protocol AccountDelegate {
|
|||
// Local account does not; some synced accounts might.
|
||||
var supportsSubFolders: Bool { get }
|
||||
var server: String? { get }
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void)
|
||||
var credentials: Credentials? { get set }
|
||||
|
||||
var refreshProgress: DownloadProgress { get }
|
||||
|
||||
|
@ -34,4 +33,7 @@ public protocol AccountDelegate {
|
|||
// Saved to disk with other Account data. Could be called at any time.
|
||||
// And called many times.
|
||||
func userInfo(for: Account) -> NSDictionary?
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void)
|
||||
|
||||
}
|
||||
|
|
|
@ -26,12 +26,12 @@ class AccountCredentialsTest: XCTestCase {
|
|||
|
||||
// Make sure any left over from failed tests are gone
|
||||
do {
|
||||
try account.removeCredentials()
|
||||
try account.removeBasicCredentials()
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
|
||||
var credentials: Credentials? = BasicCredentials(username: "maurice", password: "hardpasswd")
|
||||
var credentials: Credentials? = Credentials.basic(username: "maurice", password: "hardpasswd")
|
||||
|
||||
// Store the credentials
|
||||
do {
|
||||
|
@ -43,15 +43,19 @@ class AccountCredentialsTest: XCTestCase {
|
|||
// Retrieve them
|
||||
credentials = nil
|
||||
do {
|
||||
credentials = try account.retrieveCredentials()
|
||||
credentials = try account.retrieveBasicCredentials()
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
XCTAssertEqual("maurice", credentials!.username)
|
||||
XCTAssertEqual("hardpasswd", credentials!.password)
|
||||
|
||||
switch credentials! {
|
||||
case .basic(let username, let password):
|
||||
XCTAssertEqual("maurice", username)
|
||||
XCTAssertEqual("hardpasswd", password)
|
||||
}
|
||||
|
||||
// Update them
|
||||
credentials?.password = "easypasswd"
|
||||
credentials = Credentials.basic(username: "maurice", password: "easypasswd")
|
||||
do {
|
||||
try account.storeCredentials(credentials!)
|
||||
} catch {
|
||||
|
@ -61,23 +65,27 @@ class AccountCredentialsTest: XCTestCase {
|
|||
// Retrieve them again
|
||||
credentials = nil
|
||||
do {
|
||||
credentials = try account.retrieveCredentials()
|
||||
credentials = try account.retrieveBasicCredentials()
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
XCTAssertEqual("maurice", credentials!.username)
|
||||
XCTAssertEqual("easypasswd", credentials!.password)
|
||||
|
||||
|
||||
switch credentials! {
|
||||
case .basic(let username, let password):
|
||||
XCTAssertEqual("maurice", username)
|
||||
XCTAssertEqual("easypasswd", password)
|
||||
}
|
||||
|
||||
// Delete them
|
||||
do {
|
||||
try account.removeCredentials()
|
||||
try account.removeBasicCredentials()
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
|
||||
// Make sure they are gone
|
||||
do {
|
||||
try credentials = account.retrieveCredentials()
|
||||
try credentials = account.retrieveBasicCredentials()
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
|
|
|
@ -11,8 +11,11 @@ import RSWeb
|
|||
|
||||
struct NilTransport: Transport {
|
||||
|
||||
func send(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
completion(.success(Data()))
|
||||
func send<T>(request: URLRequest, resultType: T.Type, completion: @escaping (Result<(HTTPHeaders, T), Error>) -> Void) where T : Decodable, T : Encodable {
|
||||
}
|
||||
|
||||
|
||||
func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,13 +13,14 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")!
|
||||
private var transport: Transport!
|
||||
var credentials: Credentials?
|
||||
|
||||
init(transport: Transport) {
|
||||
super.init()
|
||||
self.transport = transport
|
||||
}
|
||||
|
||||
func validateCredentials(credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void) {
|
||||
func validateCredentials(completionHandler completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("authentication.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
@ -27,20 +28,38 @@ final class FeedbinAPICaller: NSObject {
|
|||
transport.send(request: request) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
handler(.success(true))
|
||||
completion(.success(true))
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if status == 401 {
|
||||
handler(.success(false))
|
||||
completion(.success(false))
|
||||
} else {
|
||||
handler(.failure(error))
|
||||
completion(.failure(error))
|
||||
}
|
||||
default:
|
||||
handler(.failure(error))
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveSubscriptions(completionHandler completion: @escaping (Result<[FeedbinFeed], Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [FeedbinFeed].self) { result in
|
||||
switch result {
|
||||
case .success(let (headers, feeds)):
|
||||
break // TODO: put pageing implementation here
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,17 +15,23 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
let server: String? = "api.feedbin.com"
|
||||
|
||||
private let caller: FeedbinAPICaller
|
||||
var credentials: Credentials? {
|
||||
didSet {
|
||||
caller.credentials = credentials
|
||||
}
|
||||
}
|
||||
|
||||
init(transport: Transport) {
|
||||
caller = FeedbinAPICaller(transport: transport)
|
||||
caller = FeedbinAPICaller(transport: transport)
|
||||
}
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void) {
|
||||
|
||||
let caller = FeedbinAPICaller(transport: transport)
|
||||
caller.validateCredentials(credentials: credentials) { result in
|
||||
let caller = FeedbinAPICaller(transport: transport)
|
||||
caller.credentials = credentials
|
||||
caller.validateCredentials() { result in
|
||||
handler(result)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import RSCore
|
||||
import RSParser
|
||||
|
||||
struct FeedbinFeed {
|
||||
struct FeedbinFeed: Codable {
|
||||
|
||||
// https://github.com/feedbin/feedbin-api/blob/master/content/feeds.md
|
||||
//
|
||||
|
@ -28,45 +28,13 @@ struct FeedbinFeed {
|
|||
let url: String
|
||||
let homePageURL: String?
|
||||
|
||||
struct Key {
|
||||
static let subscriptionID = "id"
|
||||
static let feedID = "feed_id"
|
||||
static let creationDate = "created_at"
|
||||
static let name = "title"
|
||||
static let url = "feed_url"
|
||||
static let homePageURL = "site_url"
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case subscriptionID = "id"
|
||||
case feedID = "feed_id"
|
||||
case creationDate = "created_at"
|
||||
case name = "title"
|
||||
case url = "feed_url"
|
||||
case homePageURL = "site_url"
|
||||
}
|
||||
|
||||
init?(dictionary: JSONDictionary) {
|
||||
|
||||
guard let subscriptionID = dictionary[Key.subscriptionID] as? Int else {
|
||||
return nil
|
||||
}
|
||||
guard let feedID = dictionary[Key.feedID] as? Int else {
|
||||
return nil
|
||||
}
|
||||
guard let url = dictionary[Key.url] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.subscriptionID = subscriptionID
|
||||
self.feedID = feedID
|
||||
self.url = url
|
||||
|
||||
if let creationDateString = dictionary[Key.creationDate] as? String {
|
||||
self.creationDate = RSDateWithString(creationDateString)
|
||||
}
|
||||
else {
|
||||
self.creationDate = nil
|
||||
}
|
||||
|
||||
self.name = dictionary[Key.name] as? String
|
||||
self.homePageURL = dictionary[Key.homePageURL] as? String
|
||||
}
|
||||
|
||||
static func feeds(with array: JSONArray) -> [FeedbinFeed]? {
|
||||
|
||||
let subs = array.compactMap { FeedbinFeed(dictionary: $0) }
|
||||
return subs.isEmpty ? nil : subs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||
|
||||
let supportsSubFolders = false
|
||||
let server: String? = nil
|
||||
var credentials: Credentials?
|
||||
|
||||
private let refresher = LocalAccountRefresher()
|
||||
|
||||
|
|
|
@ -27,9 +27,11 @@ class AccountsFeedbinWindowController: NSWindowController {
|
|||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
if let account = account, let credentials = try? account.retrieveCredentials() {
|
||||
usernameTextField.stringValue = credentials.username ?? ""
|
||||
passwordTextField.stringValue = credentials.password ?? ""
|
||||
if let account = account, let credentials = try? account.retrieveBasicCredentials() {
|
||||
if case .basic(let username, let password) = credentials {
|
||||
usernameTextField.stringValue = username
|
||||
passwordTextField.stringValue = password
|
||||
}
|
||||
actionButton.title = NSLocalizedString("Update", comment: "Update")
|
||||
} else {
|
||||
actionButton.title = NSLocalizedString("Create", comment: "Create")
|
||||
|
@ -62,7 +64,7 @@ class AccountsFeedbinWindowController: NSWindowController {
|
|||
progressIndicator.isHidden = false
|
||||
progressIndicator.startAnimation(self)
|
||||
|
||||
let credentials = BasicCredentials(username: usernameTextField.stringValue, password: passwordTextField.stringValue)
|
||||
let credentials = Credentials.basic(username: usernameTextField.stringValue, password: passwordTextField.stringValue)
|
||||
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
@ -81,7 +83,7 @@ class AccountsFeedbinWindowController: NSWindowController {
|
|||
}
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials()
|
||||
try self.account?.removeBasicCredentials()
|
||||
try self.account?.storeCredentials(credentials)
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
} catch {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 731711fff487f923d5be8ea1f4a9c19a58f059c3
|
||||
Subproject commit 64732f6b09b56374059bc3feb1ba1afb859dd0fa
|
Loading…
Reference in New Issue