Put the keychainStore inside StoreCenter + authentication refactoring

sync2
Laurent 8 months ago
parent 6748c825df
commit 26efbdd27d
  1. 76
      LeStorage/Services.swift
  2. 9
      LeStorage/Store.swift
  3. 51
      LeStorage/StoreCenter.swift

@ -43,13 +43,8 @@ public class Services {
/// The base API URL to send requests /// The base API URL to send requests
fileprivate(set) var baseURL: String fileprivate(set) var baseURL: String
/// A KeychainStore object used to store the user's token
let keychainStore: KeychainStore
public init(url: String) { public init(url: String) {
self.baseURL = url self.baseURL = url
self.keychainStore = KeychainStore(serverId: url)
Logger.log("create keystore with id: \(url)")
} }
static let storeIdURLParameter = "store_id" static let storeIdURLParameter = "store_id"
@ -262,7 +257,7 @@ public class Services {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.addAppVersion() request.addAppVersion()
if !(requiresToken == false) { if !(requiresToken == false) {
let token = try self.keychainStore.getValue() let token = try StoreCenter.main.token()
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
} }
return request return request
@ -270,28 +265,6 @@ public class Services {
// MARK: - Synchronization // 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 /// Runs a request using a traditional URLRequest
/// - Parameters: /// - Parameters:
/// - request: the URLRequest to run /// - request: the URLRequest to run
@ -379,7 +352,7 @@ public class Services {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if self._isTokenRequired(type: T.self, method: apiCall.method), StoreCenter.main.isAuthenticated { 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") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
} }
@ -421,7 +394,7 @@ public class Services {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue request.httpMethod = HTTPMethod.post.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
let modelName = String(describing: T.self) let modelName = String(describing: T.self)
@ -471,7 +444,7 @@ public class Services {
request.httpMethod = HTTPMethod.get.rawValue request.httpMethod = HTTPMethod.get.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
return request return request
@ -564,7 +537,7 @@ public class Services {
request.addAppVersion() request.addAppVersion()
if self._isTokenRequired(type: T.self, method: apiCall.method) { if self._isTokenRequired(type: T.self, method: apiCall.method) {
do { do {
let token = try self.keychainStore.getValue() let token = try StoreCenter.main.token()
request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
} catch { } catch {
Logger.log("missing token") Logger.log("missing token")
@ -613,28 +586,16 @@ public class Services {
let credentials = Credentials(username: username, password: password, deviceId: deviceId) let credentials = Credentials(username: username, password: password, deviceId: deviceId)
postRequest.httpBody = try JSON.encoder.encode(credentials) postRequest.httpBody = try JSON.encoder.encode(credentials)
let response: AuthResponse = try await self._runRequest(postRequest) 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 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 /// A login method that actually requests a token from the server, and stores the appropriate data for later usage
/// - Parameters: /// - Parameters:
/// - username: the account's username /// - username: the account's username
/// - password: the account's password /// - password: the account's password
public func login<U: UserBase>(username: String, password: String) async throws -> U { public func login<U: UserBase>(username: String, password: String) async throws -> U {
_ = try await requestToken(username: username, password: password) _ = try await requestToken(username: username, password: password)
let postRequest = try self._baseRequest(call: getUserCall) let postRequest = try self._baseRequest(call: getUserCall)
let loggingDate = Date() // ideally we want the date of the latest retrieved object when loading collection objects 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 async throws
{ {
guard let username = StoreCenter.main.userName() else { guard let username = StoreCenter.main.userName else {
throw ServiceError.missingUserName throw ServiceError.missingUserName
} }
@ -696,7 +657,7 @@ public class Services {
let response: Token = try await self._runRequest( let response: Token = try await self._runRequest(
serviceCall: changePasswordCall, payload: params) 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 /// 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) 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 /// Parse a json data and tries to extract its error message
/// - Parameters: /// - Parameters:
/// - data: some JSON data /// - data: some JSON data
@ -763,10 +709,6 @@ public class Services {
return nil return nil
} }
func migrateToken(_ services: Services, userName: String) throws {
try self._storeToken(username: userName, token: services.keychainStore.getValue())
}
// MARK: - Convenience method for tests // MARK: - Convenience method for tests
/// Executes a POST request /// Executes a POST request

@ -11,6 +11,9 @@ import UIKit
public enum StoreError: Error, LocalizedError { public enum StoreError: Error, LocalizedError {
case missingService case missingService
case missingUserId case missingUserId
case missingUsername
case missingToken
case missingKeychainStore
case collectionNotRegistered(type: String) case collectionNotRegistered(type: String)
case cannotSyncCollection(name: String) case cannotSyncCollection(name: String)
case apiCallCollectionNotRegistered(type: String) case apiCallCollectionNotRegistered(type: String)
@ -19,8 +22,14 @@ public enum StoreError: Error, LocalizedError {
switch self { switch self {
case .missingService: case .missingService:
return "Services instance is nil" return "Services instance is nil"
case .missingUsername:
return "The username is missing"
case .missingUserId: case .missingUserId:
return "The user id is missing" 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): case .collectionNotRegistered(let type):
return "The collection \(type) is not registered" return "The collection \(type) is not registered"
case .cannotSyncCollection(let name): case .cannotSyncCollection(let name):

@ -16,6 +16,9 @@ public class StoreCenter {
/// A dictionary of Stores associated to their id /// A dictionary of Stores associated to their id
fileprivate var _stores: [String: Store] = [:] 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 /// Indicates to Stored Collection if they can synchronize
public var collectionsCanSynchronize: Bool = true public var collectionsCanSynchronize: Bool = true
@ -73,6 +76,8 @@ public class StoreCenter {
let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain)
self._urlManager = urlManager self._urlManager = urlManager
self._services = Services(url: urlManager.api) self._services = Services(url: urlManager.api)
self.keychainStore = KeychainStore(serverId: urlManager.api)
self._dataAccess = Store.main.registerSynchronizedCollection() self._dataAccess = Store.main.registerSynchronizedCollection()
Logger.log("Sync URL: \(urlManager.api)") Logger.log("Sync URL: \(urlManager.api)")
@ -182,11 +187,6 @@ public class StoreCenter {
func userDidLogIn(user: UserBase, at date: Date) { func userDidLogIn(user: UserBase, at date: Date) {
self._settingsStorage.update { settings in self._settingsStorage.update { settings in
settings.userId = user.id 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) settings.lastSynchronization = Date.microSecondFormatter.string(from: date)
self._configureWebSocket() self._configureWebSocket()
} }
@ -198,18 +198,20 @@ public class StoreCenter {
} }
/// Returns the username /// Returns the username
public func userName() -> String? { public var userName: String? {
return self._settingsStorage.item.username return self._settingsStorage.item.username
} }
/// Returns the stored token /// Returns the stored token
public func token() -> String? { public func token() throws -> String {
return try? self.service().keychainStore.getValue() 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 /// Disconnect the user from the storage and resets collection
public func disconnect() { public func disconnect() {
try? self.service().deleteToken() try? self.keychainStore?.deleteValue()
self.resetApiCalls() self.resetApiCalls()
self._failedAPICallsCollection?.reset() self._failedAPICallsCollection?.reset()
@ -229,15 +231,26 @@ public class StoreCenter {
/// Returns whether the system has a user token /// Returns whether the system has a user token
public var isAuthenticated: Bool { public var isAuthenticated: Bool {
guard self.userId != nil else { return false } guard self.userName != nil else { return false }
do { do {
_ = try self.service().keychainStore.getValue() let _ = try self.token()
return true return true
} catch { } catch {
return false 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 /// Returns a generated device id
/// If created, stores it inside the keychain to get a consistent value even if the app is deleted /// 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 /// 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 /// Returns whether the current userName is allowed to sync with the server
func userIsAllowed() -> Bool { func userIsAllowed() -> Bool {
guard let userName = self.userName() else { guard let userName else { return true }
return true
}
return !self._blackListedUserName.contains(where: { $0 == userName }) return !self._blackListedUserName.contains(where: { $0 == userName })
} }
@ -1003,12 +1014,12 @@ public class StoreCenter {
// MARK: - Migration // MARK: - Migration
/// Migrates the token from the provided service to the main Services instance /// Migrates the token from the provided service to the main Services instance
public func migrateToken(_ services: Services) throws { // public func migrateToken(_ services: Services) throws {
guard let userName = self.userName() else { // guard let userName = self.userName() else {
return // return
} // }
try self.service().migrateToken(services, userName: userName) // try self.service().migrateToken(services, userName: userName)
} // }
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)

Loading…
Cancel
Save