From af4cdb36eae9847990475383c6b349c59e58183a Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 21 Feb 2024 15:21:43 +0100 Subject: [PATCH] Add layer to manage user and associated services --- LeStorage.xcodeproj/project.pbxproj | 4 ++ LeStorage/Codables/Settings.swift | 6 +- LeStorage/Services.swift | 99 ++++++++++++++++++++--------- LeStorage/Store.swift | 50 ++++++++++++--- LeStorage/Utils/Errors.swift | 12 ++++ LeStorage/Utils/FileUtils.swift | 5 -- LeStorage/Utils/KeychainStore.swift | 3 +- 7 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 LeStorage/Utils/Errors.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 34d6f8a..f6f1731 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ C4A47D942B7CF7C500ADC637 /* MicroStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D932B7CF7C500ADC637 /* MicroStorage.swift */; }; C4A47D9B2B7CFFDA00ADC637 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D992B7CFFC500ADC637 /* ApiCall.swift */; }; C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D9A2B7CFFC500ADC637 /* Settings.swift */; }; + C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAE2B85FD3800ADC637 /* Errors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -59,6 +60,7 @@ C4A47D932B7CF7C500ADC637 /* MicroStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicroStorage.swift; sourceTree = ""; }; C4A47D992B7CFFC500ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; C4A47D9A2B7CFFC500ADC637 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + C4A47DAE2B85FD3800ADC637 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -129,6 +131,7 @@ isa = PBXGroup; children = ( C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */, + C4A47DAE2B85FD3800ADC637 /* Errors.swift */, C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */, C4A47D832B7B97F000ADC637 /* KeychainStore.swift */, C4A47D522B6D2C5F00ADC637 /* Logger.swift */, @@ -268,6 +271,7 @@ C4A47D842B7B97F000ADC637 /* KeychainStore.swift in Sources */, C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */, C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */, + C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */, C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */, C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, diff --git a/LeStorage/Codables/Settings.swift b/LeStorage/Codables/Settings.swift index 1528280..c64e7d0 100644 --- a/LeStorage/Codables/Settings.swift +++ b/LeStorage/Codables/Settings.swift @@ -15,8 +15,8 @@ class Settings: MicroStorable { } - var id: String = Store.randomId() - - var userUUID: UUID? = nil +// var id: String = Store.randomId() + var userUUIDString: String? = nil + var username: String? = nil } diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index d65c9d3..ea8e31b 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -17,6 +17,7 @@ enum Method: String { enum ServiceError: Error { case urlCreationError(url: String) case cantConvertToUUID(id: String) + case missingUserName } public class Services { @@ -48,7 +49,13 @@ public class Services { // MARK: - Base - fileprivate func runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { + fileprivate func _runRequest(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U { + var request = try self._baseRequest(servicePath: servicePath, method: method) + request.httpBody = try jsonEncoder.encode(payload) + return try await _runRequest(request, apiCallId: apiCallId) + } + + fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) if let response = task.1 as? HTTPURLResponse { @@ -83,9 +90,9 @@ public class Services { var request = URLRequest(url: url) request.httpMethod = method.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") -// if let token = try? self.keychainStore.getToken() { -// request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") -// } + if let token = try? self.keychainStore.getToken() { + request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") + } return request } @@ -94,12 +101,12 @@ public class Services { func get() async throws -> [T] { let getRequest = try getRequest(servicePath: T.resourceName() + "/") - return try await self.runRequest(getRequest) + return try await self._runRequest(getRequest) } func runApiCall(_ apiCall: ApiCall) async throws -> T { let request = try self._request(from: apiCall) - return try await self.runRequest(request, apiCallId: apiCall.id) + return try await self._runRequest(request, apiCallId: apiCall.id) } fileprivate func _request(from apiCall: ApiCall) throws -> URLRequest { @@ -123,39 +130,69 @@ public class Services { // MARK: - Authentication - public func createAccount(username: String, password: String, email: String) async throws { - var postRequest = try self._baseRequest(servicePath: "users/", method: .post) - let user = User(username: username, password: password, email: email) - postRequest.httpBody = try jsonEncoder.encode(user) - let _: User = try await self.runRequest(postRequest) - let _ = try await requestToken(username: username, password: password) + public func createAccount(user: U) async throws -> V { + + let response: V = try await _runRequest(servicePath: "users/", method: .post, payload: user) + +// var postRequest = try self._baseRequest(servicePath: "users/", method: .post) +// postRequest.httpBody = try jsonEncoder.encode(user) +// let response: V = try await self.runRequest(postRequest) + let _ = try await requestToken(username: user.username, password: user.password) + Store.main.setUserName(user.username) + return response } func requestToken(username: String, password: String) async throws -> String { var postRequest = try self._baseRequest(servicePath: "plus/api-token-auth/", method: .post) let credentials = Credentials(username: username, password: password) postRequest.httpBody = try jsonEncoder.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) + return response.token + } + + fileprivate func _storeToken(username: String, token: String) { do { - try self.keychainStore.add(username: username, token: response.token) + try self.keychainStore.add(username: username, token: token) } catch { Logger.error(error) } - return response.token } - public func login(username: String, password: String) async throws -> User { + public func login(username: String, password: String) async throws -> U { let token: String = try await requestToken(username: username, password: password) Logger.log("token = \(token)") var postRequest = try self._baseRequest(servicePath: "plus/user-by-token/", method: .post) postRequest.httpBody = try jsonEncoder.encode(Token(token: token)) - let user: User = try await self.runRequest(postRequest) + let user: U = try await self._runRequest(postRequest) Logger.log("user = \(user.username), id = \(user.id)") Store.main.setUserUUID(uuidString: user.id) return user } - func forgotPassword(user: User) async throws { + public func changePassword(password1: String, password2: String) async throws { + + guard let username = Store.main.userName() else { + throw ServiceError.missingUserName + } + + struct ChangePasswordParams: Codable { + var new_password1: String + var new_password2: String + } + + let params = ChangePasswordParams(new_password1: password1, new_password2: password2) + let response: Token = try await self._runRequest( + servicePath: "plus/change-password/", method: .post, payload: params) + +// var postRequest = try self._baseRequest(servicePath: "plus/change-password/", method: .post) +// postRequest.httpBody = try jsonEncoder.encode(params) +// let response: Token = try await self._runRequest(postRequest) + + self._storeToken(username: username, token: response.token) + } + + public func forgotPassword(user: UserBase) async throws { // var postRequest = try self._baseRequest(servicePath: "forgot-password/", method: .post) // postRequest.httpBody = try jsonEncoder.encode(credentials) @@ -164,6 +201,10 @@ public class Services { // return response } + func disconnect() throws { + try self.keychainStore.deleteToken() + } + } struct AuthResponse: Codable { @@ -179,17 +220,15 @@ struct Token: Codable { var token: String } -public struct User: Codable { - var id: String = Store.randomId() - var username: String - var password: String? - var email: String? - - func uuid() throws -> UUID { - if let uuid = UUID(uuidString: self.id) { - return uuid - } - throw ServiceError.cantConvertToUUID(id: self.id) - } +public protocol UserBase : Codable { + var id: String { get } + var username: String { get } +// var password: String? { get } + var email: String? { get } + func uuid() throws -> UUID +} + +public protocol UserPasswordBase: UserBase { + var password: String { get } } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index fec4a6a..263b676 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -40,6 +40,11 @@ public class Store { /// The services performing the API calls fileprivate var _services: Services? + /// The service instance + public var service: Services? { + return self._services + } + /// The dictionary of registered StoredCollections fileprivate var _collections: [String : any SomeCollection] = [:] @@ -75,29 +80,56 @@ public class Store { return collection } + func setupServer(url: String, userModel: T) { + self.synchronizationApiURL = url + + } + + // MARK: - Settings + + func setUserUUID(uuidString: String) { + self.settingsStorage.update { settings in + settings.userUUIDString = uuidString + } + } + public func currentUserUUID() throws -> UUID { - if let uuid = self.settingsStorage.item.userUUID { + if let uuidString = self.settingsStorage.item.userUUIDString, let uuid = UUID(uuidString: uuidString) { return uuid } else { let uuid = UIDevice.current.identifierForVendor ?? UUID() self.settingsStorage.update { settings in - settings.userUUID = uuid + settings.userUUIDString = uuid.uuidString } return uuid } } - func setUserUUID(uuidString: String) { - self.settingsStorage.update { settings in - settings.userUUID = UUID(uuidString: uuidString) - } + func userName() -> String? { + return self.settingsStorage.item.username } - /// The service instance - public var service: Services? { - return self._services + func setUserName(_ username: String) { + self.settingsStorage.item.username = username + } + + public func disconnect() throws { + try self.service?.disconnect() + self.settingsStorage.item.userUUIDString = nil } + public func hasToken() -> Bool { + guard let service else { return false } + do { + _ = try service.keychainStore.getToken() + return true + } catch { + return false + } + } + + // MARK: - Convenience + /// Looks for an instance by id public func findById(_ id: String) -> T? { guard let collection = self._collections[T.resourceName()] as? StoredCollection else { diff --git a/LeStorage/Utils/Errors.swift b/LeStorage/Utils/Errors.swift new file mode 100644 index 0000000..524c779 --- /dev/null +++ b/LeStorage/Utils/Errors.swift @@ -0,0 +1,12 @@ +// +// Errors.swift +// LeStorage +// +// Created by Laurent Morvillier on 21/02/2024. +// + +import Foundation + +public enum UUIDError: Error { + case cantConvertString(string: String) +} diff --git a/LeStorage/Utils/FileUtils.swift b/LeStorage/Utils/FileUtils.swift index a9a39a0..d86fcbc 100644 --- a/LeStorage/Utils/FileUtils.swift +++ b/LeStorage/Utils/FileUtils.swift @@ -11,11 +11,6 @@ enum FileError : Error { case documentDirectoryNotFound } -enum FileFormat { - case csv - case html -} - class FileUtils { static func pathsFromDocumentsDirectory() throws -> [String] { diff --git a/LeStorage/Utils/KeychainStore.swift b/LeStorage/Utils/KeychainStore.swift index db775cf..cef6348 100644 --- a/LeStorage/Utils/KeychainStore.swift +++ b/LeStorage/Utils/KeychainStore.swift @@ -47,8 +47,7 @@ class KeychainStore { guard let existingItem = item as? [String : Any], let tokenData = existingItem[kSecValueData as String] as? Data, - let token = String(data: tokenData, encoding: .utf8) - else { + let token = String(data: tokenData, encoding: .utf8) else { throw KeychainError.unexpectedPasswordData } return token