From 92b35c7c0f032a7ce0c216f487cba8b8e3a4852a Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 13 Feb 2024 15:58:37 +0100 Subject: [PATCH] Adds service to create user and get token --- LeStorage.xcodeproj/project.pbxproj | 4 ++ LeStorage/Services.swift | 67 ++++++++++++++++++++++++++++- LeStorage/Store.swift | 2 +- LeStorage/Utils/KeychainStore.swift | 66 ++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 LeStorage/Utils/KeychainStore.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index fa490de..3a775f4 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D6C2B71364600ADC637 /* ModelObject.swift */; }; C4A47D6F2B7154F600ADC637 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = C4A47D6E2B7154F600ADC637 /* README.md */; }; C4A47D812B7665AD00ADC637 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D802B76658F00ADC637 /* Migration.swift */; }; + C4A47D842B7B97F000ADC637 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D832B7B97F000ADC637 /* KeychainStore.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +54,7 @@ C4A47D6C2B71364600ADC637 /* ModelObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelObject.swift; sourceTree = ""; }; C4A47D6E2B7154F600ADC637 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; C4A47D802B76658F00ADC637 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; + C4A47D832B7B97F000ADC637 /* KeychainStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -123,6 +125,7 @@ children = ( C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */, C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */, + C4A47D832B7B97F000ADC637 /* KeychainStore.swift */, C4A47D522B6D2C5F00ADC637 /* Logger.swift */, C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */, ); @@ -248,6 +251,7 @@ buildActionMask = 2147483647; files = ( C4A47D532B6D2C5F00ADC637 /* Logger.swift in Sources */, + C4A47D842B7B97F000ADC637 /* KeychainStore.swift in Sources */, C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */, C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */, C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */, diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 88cc9dc..089b7a1 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -18,14 +18,25 @@ enum ServiceError: Error { case urlCreationError(url: String) } -class Services { +public class Services { + + let keychainStore: KeychainStore init(url: String) { self.baseURL = url + self.keychainStore = KeychainStore(serverId: url) } fileprivate(set) var baseURL: String + fileprivate var jsonEncoder: JSONEncoder = { + let encoder = JSONEncoder() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + encoder.dateEncodingStrategy = .formatted(dateFormatter) + return encoder + }() + fileprivate var jsonDecoder: JSONDecoder = { let decoder = JSONDecoder() let dateFormatter = DateFormatter() @@ -94,7 +105,61 @@ class Services { request.httpMethod = apiCall.method request.httpBody = apiCall.body.data(using: .utf8) request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + do { + let token = try self.keychainStore.getToken() + request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") + } catch { + Logger.log("missing token") + } + return request } + + // 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(user: user) + } + + func requestToken(user: User) async throws -> String { + var postRequest = try self._baseRequest(servicePath: "api-token-auth/", method: .post) + postRequest.httpBody = try jsonEncoder.encode(user) + let response: AuthResponse = try await self.runRequest(postRequest) + try self.keychainStore.add(username: user.username, token: response.token) + return response.token + } + + func forgotPassword(user: User) async throws { + +// var postRequest = try self._baseRequest(servicePath: "forgot-password/", method: .post) +// postRequest.httpBody = try jsonEncoder.encode(credentials) +// let response: User = try await self.runRequest(postRequest) +// let _ = try await requestToken(credentials: credentials) +// return response + } + + + +} + +struct AuthResponse: Codable { + let token: String +} + +struct Credentials: Codable { + var username: String + var password: String +} +struct User: Codable { + var id: Int = 0 + var username: String + var password: String? + var email: String? } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 214eb9e..99da9d8 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -71,7 +71,7 @@ public class Store { } /// The service instance - var service: Services? { + public var service: Services? { return self._services } diff --git a/LeStorage/Utils/KeychainStore.swift b/LeStorage/Utils/KeychainStore.swift new file mode 100644 index 0000000..5e0d8c4 --- /dev/null +++ b/LeStorage/Utils/KeychainStore.swift @@ -0,0 +1,66 @@ +// +// KeychainStore.swift +// LeCountdown +// +// Created by Laurent Morvillier on 20/12/2023. +// + +import Foundation + +enum KeychainError: Error { + case noPassword + case unexpectedPasswordData + case unhandledError(status: OSStatus) +} + +class KeychainStore { + + let serverId: String + + init(serverId: String) { + self.serverId = serverId + } + + func add(username: String, token: String) throws { + let tokenData = token.data(using: .utf8)! + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccount as String: username, + kSecAttrServer as String: self.serverId, + kSecValueData as String: tokenData] + + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } + } + + func getToken() throws -> String { + + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: self.serverId, + 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 { throw KeychainError.noPassword } + guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } + + guard let existingItem = item as? [String : Any], + let tokenData = existingItem[kSecValueData as String] as? Data, + let token = String(data: tokenData, encoding: .utf8) + else { + throw KeychainError.unexpectedPasswordData + } + return token + } + + func deleteToken() throws { + + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: self.serverId] + + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } + } + +}