Add layer to manage user and associated services

multistore
Laurent 2 years ago
parent cdbd9566c9
commit af4cdb36ea
  1. 4
      LeStorage.xcodeproj/project.pbxproj
  2. 6
      LeStorage/Codables/Settings.swift
  3. 99
      LeStorage/Services.swift
  4. 50
      LeStorage/Store.swift
  5. 12
      LeStorage/Utils/Errors.swift
  6. 5
      LeStorage/Utils/FileUtils.swift
  7. 3
      LeStorage/Utils/KeychainStore.swift

@ -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 = "<group>"; };
C4A47D992B7CFFC500ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = "<group>"; };
C4A47D9A2B7CFFC500ADC637 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
C4A47DAE2B85FD3800ADC637 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = "<group>"; };
/* 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 */,

@ -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
}

@ -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<T : Decodable>(_ request: URLRequest, apiCallId: String? = nil) async throws -> T {
fileprivate func _runRequest<T : Encodable, U : Decodable>(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<T : Decodable>(_ 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<T : Storable>() async throws -> [T] {
let getRequest = try getRequest(servicePath: T.resourceName() + "/")
return try await self.runRequest(getRequest)
return try await self._runRequest(getRequest)
}
func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) 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<T : Storable>(from apiCall: ApiCall<T>) 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<U : UserPasswordBase, V : UserBase>(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<U : UserBase>(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 }
}

@ -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<T : UserBase>(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<T : Storable>(_ id: String) -> T? {
guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {

@ -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)
}

@ -11,11 +11,6 @@ enum FileError : Error {
case documentDirectoryNotFound
}
enum FileFormat {
case csv
case html
}
class FileUtils {
static func pathsFromDocumentsDirectory() throws -> [String] {

@ -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

Loading…
Cancel
Save