diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 3148276..88fb46f 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -43,13 +43,8 @@ public class Services { /// The base API URL to send requests fileprivate(set) var baseURL: String - /// A KeychainStore object used to store the user's token - let keychainStore: KeychainStore - public init(url: String) { self.baseURL = url - self.keychainStore = KeychainStore(serverId: url) - Logger.log("create keystore with id: \(url)") } static let storeIdURLParameter = "store_id" @@ -228,14 +223,14 @@ public class Services { // let requiresToken = self._isTokenRequired(type: T.self, method: .delete) // return try self._baseRequest(servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken) // } - - /// Returns the base URLRequest for a ServiceConf instance - /// - Parameters: - /// - conf: a ServiceConf instance - fileprivate func _baseRequest(call: ServiceCall) throws -> URLRequest { - return try self._baseRequest(servicePath: call.path, method: call.method, requiresToken: call.requiresToken) - } - + + /// Returns the base URLRequest for a ServiceConf instance + /// - Parameters: + /// - conf: a ServiceConf instance + fileprivate func _baseRequest(call: ServiceCall) throws -> URLRequest { + return try self._baseRequest(servicePath: call.path, method: call.method, requiresToken: call.requiresToken) + } + /// Returns a base request for a path and method /// - Parameters: /// - servicePath: the path to add to the API base URL @@ -262,7 +257,7 @@ public class Services { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.addAppVersion() if !(requiresToken == false) { - let token = try self.keychainStore.getValue() + let token = try StoreCenter.main.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") } return request @@ -270,28 +265,6 @@ public class Services { // MARK: - Synchronization - /// Returns a base request for a path and method - /// - Parameters: - /// - method: the HTTP method to execute - /// - payload: the content to put in the httpBody -// fileprivate func _baseSyncRequest(method: HTTPMethod, payload: Encodable) throws -> URLRequest { -// let urlString = baseURL + "data/" -// -// guard let url = URL(string: urlString) else { -// throw ServiceError.urlCreationError(url: urlString) -// } -// -// var request = URLRequest(url: url) -// request.httpMethod = method.rawValue -// request.setValue("application/json", forHTTPHeaderField: "Content-Type") -// request.httpBody = try JSON.encoder.encode(payload) -// -// let token = try self.keychainStore.getValue() -// request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") -// -// return request -// } - /// Runs a request using a traditional URLRequest /// - Parameters: /// - request: the URLRequest to run @@ -379,7 +352,7 @@ public class Services { request.setValue("application/json", forHTTPHeaderField: "Content-Type") if self._isTokenRequired(type: T.self, method: apiCall.method), StoreCenter.main.isAuthenticated { - let token = try self.keychainStore.getValue() + let token = try StoreCenter.main.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") } @@ -421,7 +394,7 @@ public class Services { var request = URLRequest(url: url) request.httpMethod = HTTPMethod.post.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") - let token = try self.keychainStore.getValue() + let token = try StoreCenter.main.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") let modelName = String(describing: T.self) @@ -471,7 +444,7 @@ public class Services { request.httpMethod = HTTPMethod.get.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") - let token = try self.keychainStore.getValue() + let token = try StoreCenter.main.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") return request @@ -564,7 +537,7 @@ public class Services { request.addAppVersion() if self._isTokenRequired(type: T.self, method: apiCall.method) { do { - let token = try self.keychainStore.getValue() + let token = try StoreCenter.main.token() request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") } catch { Logger.log("missing token") @@ -613,28 +586,16 @@ public class Services { let credentials = Credentials(username: username, password: password, deviceId: deviceId) postRequest.httpBody = try JSON.encoder.encode(credentials) let response: AuthResponse = try await self._runRequest(postRequest) - self._storeToken(username: username, token: response.token) + try StoreCenter.main.storeToken(username: username, token: response.token) return response.token } - /// Stores a token for a corresponding username - /// - Parameters: - /// - username: the key used to store the token - /// - token: the token to store - fileprivate func _storeToken(username: String, token: String) { - do { - try self.keychainStore.deleteValue() - try self.keychainStore.add(username: username, value: token) - } catch { - Logger.error(error) - } - } - /// A login method that actually requests a token from the server, and stores the appropriate data for later usage /// - Parameters: /// - username: the account's username /// - password: the account's password public func login(username: String, password: String) async throws -> U { + _ = try await requestToken(username: username, password: password) let postRequest = try self._baseRequest(call: getUserCall) let loggingDate = Date() // ideally we want the date of the latest retrieved object when loading collection objects @@ -681,7 +642,7 @@ public class Services { async throws { - guard let username = StoreCenter.main.userName() else { + guard let username = StoreCenter.main.userName else { throw ServiceError.missingUserName } @@ -696,7 +657,7 @@ public class Services { let response: Token = try await self._runRequest( serviceCall: changePasswordCall, payload: params) - self._storeToken(username: username, token: response.token) + try StoreCenter.main.storeToken(username: username, token: response.token) } /// The method send a request to reset the user's password @@ -724,21 +685,6 @@ public class Services { let _: Empty = try await self._runRequest(request) } - /// Deletes the locally stored token - func deleteToken() throws { - try self.keychainStore.deleteValue() - } - - /// Returns whether the Service has an associated token - public func hasToken() -> Bool { - do { - _ = try self.keychainStore.getValue() - return true - } catch { - return false - } - } - /// Parse a json data and tries to extract its error message /// - Parameters: /// - data: some JSON data @@ -763,10 +709,6 @@ public class Services { return nil } - func migrateToken(_ services: Services, userName: String) throws { - try self._storeToken(username: userName, token: services.keychainStore.getValue()) - } - // MARK: - Convenience method for tests /// Executes a POST request diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index c825e35..d1fc9eb 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -11,6 +11,9 @@ import UIKit public enum StoreError: Error, LocalizedError { case missingService case missingUserId + case missingUsername + case missingToken + case missingKeychainStore case collectionNotRegistered(type: String) case cannotSyncCollection(name: String) case apiCallCollectionNotRegistered(type: String) @@ -19,8 +22,14 @@ public enum StoreError: Error, LocalizedError { switch self { case .missingService: return "Services instance is nil" + case .missingUsername: + return "The username is missing" case .missingUserId: return "The user id is missing" + case .missingToken: + return "There is no stored token" + case .missingKeychainStore: + return "There is no keychain store" case .collectionNotRegistered(let type): return "The collection \(type) is not registered" case .cannotSyncCollection(let name): diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 1c59133..73b66f1 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -16,6 +16,9 @@ public class StoreCenter { /// A dictionary of Stores associated to their id fileprivate var _stores: [String: Store] = [:] + /// A KeychainStore object used to store the user's token + var keychainStore: KeychainStore? = nil + /// Indicates to Stored Collection if they can synchronize public var collectionsCanSynchronize: Bool = true @@ -73,6 +76,8 @@ public class StoreCenter { let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) self._urlManager = urlManager self._services = Services(url: urlManager.api) + self.keychainStore = KeychainStore(serverId: urlManager.api) + self._dataAccess = Store.main.registerSynchronizedCollection() Logger.log("Sync URL: \(urlManager.api)") @@ -182,11 +187,6 @@ public class StoreCenter { func userDidLogIn(user: UserBase, at date: Date) { self._settingsStorage.update { settings in settings.userId = user.id - settings.username = user.username - -// let date = Date.microSecondFormatter.string(from: date) -// Logger.log("LOG date = \(date)") - settings.lastSynchronization = Date.microSecondFormatter.string(from: date) self._configureWebSocket() } @@ -198,18 +198,20 @@ public class StoreCenter { } /// Returns the username - public func userName() -> String? { + public var userName: String? { return self._settingsStorage.item.username } /// Returns the stored token - public func token() -> String? { - return try? self.service().keychainStore.getValue() + public func token() throws -> String { + guard self.userName != nil else { throw StoreError.missingUsername } + guard let keychainStore else { throw StoreError.missingKeychainStore } + return try keychainStore.getValue() } /// Disconnect the user from the storage and resets collection public func disconnect() { - try? self.service().deleteToken() + try? self.keychainStore?.deleteValue() self.resetApiCalls() self._failedAPICallsCollection?.reset() @@ -229,15 +231,26 @@ public class StoreCenter { /// Returns whether the system has a user token public var isAuthenticated: Bool { - guard self.userId != nil else { return false } + guard self.userName != nil else { return false } do { - _ = try self.service().keychainStore.getValue() + let _ = try self.token() return true } catch { return false } } + /// Stores a token for a corresponding username + /// - Parameters: + /// - username: the key used to store the token + /// - token: the token to store + func storeToken(username: String, token: String) throws { + self._settingsStorage.item.username = username + guard let keychainStore else { throw StoreError.missingKeychainStore } + try keychainStore.deleteValue() + try keychainStore.add(username: username, value: token) + } + /// Returns a generated device id /// If created, stores it inside the keychain to get a consistent value even if the app is deleted /// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again @@ -882,9 +895,7 @@ public class StoreCenter { /// Returns whether the current userName is allowed to sync with the server func userIsAllowed() -> Bool { - guard let userName = self.userName() else { - return true - } + guard let userName else { return true } return !self._blackListedUserName.contains(where: { $0 == userName }) } @@ -1003,12 +1014,12 @@ public class StoreCenter { // MARK: - Migration /// Migrates the token from the provided service to the main Services instance - public func migrateToken(_ services: Services) throws { - guard let userName = self.userName() else { - return - } - try self.service().migrateToken(services, userName: userName) - } +// public func migrateToken(_ services: Services) throws { +// guard let userName = self.userName() else { +// return +// } +// try self.service().migrateToken(services, userName: userName) +// } deinit { NotificationCenter.default.removeObserver(self)