|
|
|
|
@ -20,64 +20,35 @@ struct ServiceCall { |
|
|
|
|
var requiresToken: Bool |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let createAccountCall: ServiceCall = ServiceCall(path: "users/", method: .post, requiresToken: false) |
|
|
|
|
let requestTokenCall: ServiceCall = ServiceCall(path: "token-auth/", method: .post, requiresToken: false) |
|
|
|
|
let logoutCall: ServiceCall = ServiceCall(path: "api-token-logout/", method: .post, requiresToken: true) |
|
|
|
|
let getUserCall: ServiceCall = ServiceCall(path: "user-by-token/", method: .get, requiresToken: true) |
|
|
|
|
let changePasswordCall: ServiceCall = ServiceCall(path: "change-password/", method: .put, requiresToken: true) |
|
|
|
|
let postDeviceTokenCall: ServiceCall = ServiceCall(path: "device-token/", method: .post, requiresToken: true) |
|
|
|
|
|
|
|
|
|
//fileprivate enum ServiceConf: String { |
|
|
|
|
// case createAccount = "users/" |
|
|
|
|
// case requestToken = "token-auth/" |
|
|
|
|
// case logout = "api-token-logout/" |
|
|
|
|
// case getUser = "user-by-token/" |
|
|
|
|
// case changePassword = "change-password/" |
|
|
|
|
// case postDeviceToken = "device-token/" |
|
|
|
|
// |
|
|
|
|
// var method: HTTPMethod { |
|
|
|
|
// switch self { |
|
|
|
|
// case .createAccount, .requestToken, .logout, .postDeviceToken: |
|
|
|
|
// return .post |
|
|
|
|
// case .changePassword: |
|
|
|
|
// return .put |
|
|
|
|
// default: |
|
|
|
|
// return .get |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
// |
|
|
|
|
// var requiresToken: Bool? { |
|
|
|
|
// switch self { |
|
|
|
|
// case .createAccount, .requestToken: |
|
|
|
|
// return false |
|
|
|
|
// case .getUser, .changePassword, .logout, .postDeviceToken: |
|
|
|
|
// return true |
|
|
|
|
//// default: |
|
|
|
|
//// return nil |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
// |
|
|
|
|
//} |
|
|
|
|
let createAccountCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "users/", method: .post, requiresToken: false) |
|
|
|
|
let requestTokenCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "token-auth/", method: .post, requiresToken: false) |
|
|
|
|
let logoutCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "api-token-logout/", method: .post, requiresToken: true) |
|
|
|
|
let getUserCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "user-by-token/", method: .get, requiresToken: true) |
|
|
|
|
let changePasswordCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "change-password/", method: .put, requiresToken: true) |
|
|
|
|
let postDeviceTokenCall: ServiceCall = ServiceCall( |
|
|
|
|
path: "device-token/", method: .post, requiresToken: true) |
|
|
|
|
|
|
|
|
|
/// A class used to send HTTP request to the django server |
|
|
|
|
public class Services { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A KeychainStore object used to store the user's token |
|
|
|
|
let keychainStore: KeychainStore |
|
|
|
|
|
|
|
|
|
// fileprivate var _storeIdentifier: StoreIdentifier? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public init(url: String) { |
|
|
|
|
self.baseURL = url |
|
|
|
|
self.keychainStore = KeychainStore(serverId: url) |
|
|
|
|
// self._storeIdentifier = storeId |
|
|
|
|
Logger.log("create keystore with id: \(url)") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The base API URL to send requests |
|
|
|
|
fileprivate(set) var baseURL: String |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate var jsonEncoder: JSONEncoder = { |
|
|
|
|
let encoder = JSONEncoder() |
|
|
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase |
|
|
|
|
@ -92,7 +63,7 @@ public class Services { |
|
|
|
|
decoder.dateDecodingStrategy = .iso8601 |
|
|
|
|
return decoder |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Base |
|
|
|
|
|
|
|
|
|
/// Runs a request using a configuration object |
|
|
|
|
@ -100,40 +71,47 @@ public class Services { |
|
|
|
|
/// - serviceConf: A instance of ServiceConf |
|
|
|
|
/// - payload: a codable value stored in the body of the request |
|
|
|
|
/// - apiCallId: an optional id referencing an ApiCall |
|
|
|
|
fileprivate func _runRequest<T: Encodable, U: Decodable>(serviceCall: ServiceCall, payload: T) async throws -> U { |
|
|
|
|
fileprivate func _runRequest<T: Encodable, U: Decodable>(serviceCall: ServiceCall, payload: T) |
|
|
|
|
async throws -> U |
|
|
|
|
{ |
|
|
|
|
var request = try self._baseRequest(call: serviceCall) |
|
|
|
|
request.httpBody = try jsonEncoder.encode(payload) |
|
|
|
|
return try await _runRequest(request) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Runs a request using a traditional URLRequest |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - request: the URLRequest to run |
|
|
|
|
/// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure |
|
|
|
|
fileprivate func _runRequest<T: Storable, V: Decodable>(_ request: URLRequest, apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
fileprivate func _runRequest<T: SyncedStorable, V: Decodable>( |
|
|
|
|
_ request: URLRequest, apiCall: ApiCall<T> |
|
|
|
|
) async throws -> V { |
|
|
|
|
let debugURL = request.url?.absoluteString ?? "" |
|
|
|
|
print("Run \(request.httpMethod ?? "") \(debugURL)") |
|
|
|
|
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) |
|
|
|
|
print("response = \(String(data: task.0, encoding: .utf8) ?? "")") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let response = task.1 as? HTTPURLResponse { |
|
|
|
|
let statusCode = response.statusCode |
|
|
|
|
print("\(debugURL) ended, status code = \(statusCode)") |
|
|
|
|
switch statusCode { |
|
|
|
|
case 200..<300: // success |
|
|
|
|
try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id) |
|
|
|
|
default: // error |
|
|
|
|
Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
case 200..<300: // success |
|
|
|
|
try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id) |
|
|
|
|
default: // error |
|
|
|
|
Logger.log( |
|
|
|
|
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
let errorString: String = String(data: task.0, encoding: .utf8) ?? "" |
|
|
|
|
var errorMessage = ErrorMessage(error: errorString, domain: "") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let message = self.errorMessageFromResponse(data: task.0) { |
|
|
|
|
errorMessage = message |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self) |
|
|
|
|
StoreCenter.main.logFailedAPICall(apiCall.id, request: request, collectionName: T.resourceName(), error: errorMessage.message) |
|
|
|
|
|
|
|
|
|
StoreCenter.main.logFailedAPICall( |
|
|
|
|
apiCall.id, request: request, collectionName: T.resourceName(), |
|
|
|
|
error: errorMessage.message) |
|
|
|
|
|
|
|
|
|
throw ServiceError.responseError(response: errorMessage.error) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
@ -141,14 +119,14 @@ public class Services { |
|
|
|
|
StoreCenter.main.log(message: message) |
|
|
|
|
Logger.w(message) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !(V.self is Empty?.Type) { |
|
|
|
|
return try jsonDecoder.decode(V.self, from: task.0) |
|
|
|
|
} else { |
|
|
|
|
return try jsonDecoder.decode(V.self, from: "{}".data(using: .utf8)!) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Runs a request using a traditional URLRequest |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - request: the URLRequest to run |
|
|
|
|
@ -158,15 +136,16 @@ public class Services { |
|
|
|
|
print("Run \(request.httpMethod ?? "") \(debugURL)") |
|
|
|
|
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) |
|
|
|
|
print("response = \(String(data: task.0, encoding: .utf8) ?? "")") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let response = task.1 as? HTTPURLResponse { |
|
|
|
|
let statusCode = response.statusCode |
|
|
|
|
print("\(debugURL) ended, status code = \(statusCode)") |
|
|
|
|
switch statusCode { |
|
|
|
|
case 200..<300: // success |
|
|
|
|
case 200..<300: // success |
|
|
|
|
break |
|
|
|
|
default: // error |
|
|
|
|
Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
default: // error |
|
|
|
|
Logger.log( |
|
|
|
|
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
let errorString: String = String(data: task.0, encoding: .utf8) ?? "" |
|
|
|
|
var errorMessage = ErrorMessage(error: errorString, domain: "") |
|
|
|
|
if let message = self.errorMessageFromResponse(data: task.0) { |
|
|
|
|
@ -181,12 +160,12 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
return try jsonDecoder.decode(V.self, from: task.0) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns if the token is required for a request |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
/// - method: the HTTP method of the request |
|
|
|
|
fileprivate func _isTokenRequired<T : Storable>(type: T.Type, method: HTTPMethod) -> Bool { |
|
|
|
|
fileprivate func _isTokenRequired<T: SyncedStorable>(type: T.Type, method: HTTPMethod) -> Bool { |
|
|
|
|
let methods = T.tokenExemptedMethods() |
|
|
|
|
if methods.contains(method) { |
|
|
|
|
return false |
|
|
|
|
@ -194,53 +173,67 @@ public class Services { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a GET request for the resource |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
fileprivate func _getRequest<T: Storable>(type: T.Type, identifier: StoreIdentifier?) throws -> URLRequest { |
|
|
|
|
fileprivate func _getRequest<T: SyncedStorable>(type: T.Type, identifier: StoreIdentifier?) |
|
|
|
|
throws |
|
|
|
|
-> URLRequest |
|
|
|
|
{ |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .get) |
|
|
|
|
return try self._baseRequest(servicePath: T.path(), method: .get, requiresToken: requiresToken, identifier: identifier) |
|
|
|
|
return try self._baseRequest( |
|
|
|
|
servicePath: T.path(), method: .get, requiresToken: requiresToken, |
|
|
|
|
identifier: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a POST request for the resource |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
fileprivate func _postRequest<T: Storable>(type: T.Type) throws -> URLRequest { |
|
|
|
|
fileprivate func _postRequest<T: SyncedStorable>(type: T.Type) throws -> URLRequest { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .post) |
|
|
|
|
return try self._baseRequest(servicePath: T.path(), method: .post, requiresToken: requiresToken) |
|
|
|
|
return try self._baseRequest( |
|
|
|
|
servicePath: T.path(), method: .post, requiresToken: requiresToken) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a PUT request for the resource |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
fileprivate func _putRequest<T: Storable>(type: T.Type, id: String) throws -> URLRequest { |
|
|
|
|
fileprivate func _putRequest<T: SyncedStorable>(type: T.Type, id: String) throws -> URLRequest { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .put) |
|
|
|
|
return try self._baseRequest(servicePath: T.path(id: id), method: .put, requiresToken: requiresToken) |
|
|
|
|
return try self._baseRequest( |
|
|
|
|
servicePath: T.path(id: id), method: .put, requiresToken: requiresToken) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a DELETE request for the resource |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
fileprivate func _deleteRequest<T: Storable>(type: T.Type, id: String) throws -> URLRequest { |
|
|
|
|
fileprivate func _deleteRequest<T: SyncedStorable>(type: T.Type, id: String) throws |
|
|
|
|
-> URLRequest |
|
|
|
|
{ |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .delete) |
|
|
|
|
return try self._baseRequest(servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken) |
|
|
|
|
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) |
|
|
|
|
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 |
|
|
|
|
/// - method: the HTTP method to execute |
|
|
|
|
/// - requiresToken: An optional boolean to indicate if the token is required |
|
|
|
|
/// - identifier: an optional StoreIdentifier that allows to filter GET requests with the StoreIdentifier values |
|
|
|
|
fileprivate func _baseRequest(servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil, identifier: StoreIdentifier? = nil) throws -> URLRequest { |
|
|
|
|
fileprivate func _baseRequest( |
|
|
|
|
servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil, |
|
|
|
|
identifier: StoreIdentifier? = nil |
|
|
|
|
) throws -> URLRequest { |
|
|
|
|
var urlString = baseURL + servicePath |
|
|
|
|
if let identifier { |
|
|
|
|
urlString.append(identifier.urlComponent) |
|
|
|
|
@ -257,46 +250,192 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 jsonEncoder.encode(payload) |
|
|
|
|
|
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: An ApiCall instance to configure the returned request |
|
|
|
|
fileprivate func _syncRequest<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest |
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
let urlString = baseURL + "data/" |
|
|
|
|
|
|
|
|
|
guard let url = URL(string: urlString) else { |
|
|
|
|
throw ServiceError.urlCreationError(url: urlString) |
|
|
|
|
} |
|
|
|
|
guard let bodyData = apiCall.body.data(using: .utf8) else { |
|
|
|
|
throw ServiceError.cantDecodeData(content: apiCall.body) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
|
request.httpMethod = HTTPMethod.post.rawValue |
|
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
|
|
|
|
|
// moyennement fan de decoder pour recoder derriere |
|
|
|
|
let data = try jsonDecoder.decode(T.self, from: bodyData) |
|
|
|
|
let modelName = String(describing: T.self) |
|
|
|
|
|
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: apiCall.method.rawValue, |
|
|
|
|
modelName: modelName, |
|
|
|
|
data: data, |
|
|
|
|
storeId: data.getStoreId()) |
|
|
|
|
|
|
|
|
|
request.httpBody = try jsonEncoder.encode(payload) |
|
|
|
|
|
|
|
|
|
if self._isTokenRequired(type: T.self, method: apiCall.method) { |
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Starts a request to retrieve the synchronization updates |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - since: The date from which updates are retrieved |
|
|
|
|
func synchronizeLastUpdates(since: Date?) async throws { |
|
|
|
|
let request = try self._getSyncLogRequest(since: since) |
|
|
|
|
try await self._runGetSyncLogRequest(request) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - since: The date from which updates are retrieved |
|
|
|
|
fileprivate func _getSyncLogRequest(since: Date?) throws -> URLRequest { |
|
|
|
|
|
|
|
|
|
let formattedDate = ISO8601DateFormatter().string(from: since ?? Date.distantPast) |
|
|
|
|
let encodedDate = |
|
|
|
|
formattedDate.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" |
|
|
|
|
let encodedDateWithPlus = encodedDate.replacingOccurrences(of: "+", with: "%2B") |
|
|
|
|
let urlString = baseURL + "data/?last_update=\(encodedDateWithPlus)" |
|
|
|
|
|
|
|
|
|
guard let url = URL(string: urlString) else { |
|
|
|
|
throw ServiceError.urlCreationError(url: urlString) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
|
request.httpMethod = HTTPMethod.get.rawValue |
|
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
|
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Runs the a sync request and forwards the response to the StoreCenter for processing |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - request: The synchronization request |
|
|
|
|
fileprivate func _runGetSyncLogRequest(_ request: URLRequest) async throws { |
|
|
|
|
let debugURL = request.url?.absoluteString ?? "" |
|
|
|
|
print("Run \(request.httpMethod ?? "") \(debugURL)") |
|
|
|
|
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) |
|
|
|
|
print("response = \(String(data: task.0, encoding: .utf8) ?? "")") |
|
|
|
|
|
|
|
|
|
if let response = task.1 as? HTTPURLResponse { |
|
|
|
|
let statusCode = response.statusCode |
|
|
|
|
print("\(debugURL) ended, status code = \(statusCode)") |
|
|
|
|
switch statusCode { |
|
|
|
|
case 200..<300: // success |
|
|
|
|
StoreCenter.main.synchronizeContent(task.0, decoder: self.jsonDecoder) |
|
|
|
|
default: // error |
|
|
|
|
Logger.log( |
|
|
|
|
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
let errorString: String = String(data: task.0, encoding: .utf8) ?? "" |
|
|
|
|
var errorMessage = ErrorMessage(error: errorString, domain: "") |
|
|
|
|
if let message = self.errorMessageFromResponse(data: task.0) { |
|
|
|
|
errorMessage = message |
|
|
|
|
} |
|
|
|
|
throw ServiceError.responseError(response: errorMessage.error) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let message: String = "Unexpected and unmanaged URL Response \(task.1)" |
|
|
|
|
StoreCenter.main.log(message: message) |
|
|
|
|
Logger.w(message) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Services |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes a GET request |
|
|
|
|
public func get<T: Storable>(identifier: StoreIdentifier? = nil) async throws -> [T] { |
|
|
|
|
public func get<T: SyncedStorable>(identifier: StoreIdentifier? = nil) async throws -> [T] { |
|
|
|
|
let getRequest = try _getRequest(type: T.self, identifier: identifier) |
|
|
|
|
return try await self._runRequest(getRequest) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Executes a POST request |
|
|
|
|
public func post<T: Storable>(_ instance: T) async throws -> T { |
|
|
|
|
var postRequest = try self._postRequest(type: T.self) |
|
|
|
|
postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
return try await self._runRequest(postRequest) |
|
|
|
|
|
|
|
|
|
let method: HTTPMethod = .post |
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: method.rawValue, |
|
|
|
|
modelName: String(describing: T.self), |
|
|
|
|
data: instance) |
|
|
|
|
let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
return try await self._runRequest(syncRequest) |
|
|
|
|
|
|
|
|
|
// var postRequest = try self._postRequest(type: T.self) |
|
|
|
|
// postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
// return try await self._runRequest(postRequest) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes a PUT request |
|
|
|
|
public func put<T: Storable>(_ instance: T) async throws -> T { |
|
|
|
|
var postRequest = try self._putRequest(type: T.self, id: instance.stringId) |
|
|
|
|
postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
return try await self._runRequest(postRequest) |
|
|
|
|
public func put<T: SyncedStorable>(_ instance: T) async throws -> T { |
|
|
|
|
|
|
|
|
|
let method: HTTPMethod = .put |
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: method.rawValue, |
|
|
|
|
modelName: String(describing: T.self), |
|
|
|
|
data: instance) |
|
|
|
|
let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
return try await self._runRequest(syncRequest) |
|
|
|
|
|
|
|
|
|
// var postRequest = try self._putRequest(type: T.self, id: instance.stringId) |
|
|
|
|
// postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
// return try await self._runRequest(postRequest) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an ApiCall |
|
|
|
|
func runApiCall<T: Storable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
let request = try self._request(from: apiCall) |
|
|
|
|
func runApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
let request = try self._syncRequest(from: apiCall) |
|
|
|
|
print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") |
|
|
|
|
return try await self._runRequest(request, apiCall: apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: An ApiCall instance to configure the returned request |
|
|
|
|
fileprivate func _request<T: Storable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
fileprivate func _request<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
let url = try self._url(from: apiCall) |
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
|
request.httpMethod = apiCall.method.rawValue |
|
|
|
|
request.httpBody = apiCall.body.data(using: .utf8) |
|
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self._isTokenRequired(type: T.self, method: apiCall.method) { |
|
|
|
|
do { |
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
@ -305,10 +444,10 @@ public class Services { |
|
|
|
|
Logger.log("missing token") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the URL corresponding to the ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: an instance of ApiCall to build to URL |
|
|
|
|
@ -326,16 +465,16 @@ public class Services { |
|
|
|
|
throw ServiceError.urlCreationError(url: stringURL) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Authentication |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Creates an account |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - user: A user instance to send to the server |
|
|
|
|
public func createAccount<U: UserPasswordBase, V: UserBase>(user: U) async throws -> V { |
|
|
|
|
return try await _runRequest(serviceCall: createAccountCall, payload: user) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Requests a token for a username and password |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - username: the account's username |
|
|
|
|
@ -349,7 +488,7 @@ public class Services { |
|
|
|
|
self._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 |
|
|
|
|
@ -362,94 +501,100 @@ public class Services { |
|
|
|
|
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<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) |
|
|
|
|
let postRequest = try self._baseRequest(call: getUserCall) |
|
|
|
|
let user: U = try await self._runRequest(postRequest) |
|
|
|
|
// StoreCenter.main.setUserUUID(uuidString: user.id) |
|
|
|
|
// StoreCenter.main.setUserName(user.username) |
|
|
|
|
// StoreCenter.main.setUserUUID(uuidString: user.id) |
|
|
|
|
// StoreCenter.main.setUserName(user.username) |
|
|
|
|
StoreCenter.main.setUserInfo(user: user) |
|
|
|
|
return user |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 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 logout() async throws { |
|
|
|
|
public func logout() async throws { |
|
|
|
|
let deviceId: String = StoreCenter.main.deviceId() |
|
|
|
|
let _: Empty = try await self._runRequest(serviceCall: logoutCall, payload: Logout(deviceId: deviceId)) |
|
|
|
|
let _: Empty = try await self._runRequest( |
|
|
|
|
serviceCall: logoutCall, payload: Logout(deviceId: deviceId)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 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 postDeviceToken(deviceToken: Data) async throws { |
|
|
|
|
public func postDeviceToken(deviceToken: Data) async throws { |
|
|
|
|
let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined() |
|
|
|
|
let token = DeviceToken(value: tokenString) |
|
|
|
|
// Logger.log("Send device token = \(tokenString)") |
|
|
|
|
// Logger.log("Send device token = \(tokenString)") |
|
|
|
|
let _: Empty = try await self._runRequest(serviceCall: postDeviceTokenCall, payload: token) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A method that sends a request to change a user's password |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - oldPassword: the account's old password |
|
|
|
|
/// - password1: the account's new password |
|
|
|
|
/// - password2: a repeat of the new password |
|
|
|
|
public func changePassword(oldPassword: String, password1: String, password2: String) async throws { |
|
|
|
|
|
|
|
|
|
public func changePassword(oldPassword: String, password1: String, password2: String) |
|
|
|
|
async throws |
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
guard let username = StoreCenter.main.userName() else { |
|
|
|
|
throw ServiceError.missingUserName |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct ChangePasswordParams: Codable { |
|
|
|
|
var old_password: String |
|
|
|
|
var new_password1: String |
|
|
|
|
var new_password2: String |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let params = ChangePasswordParams(old_password: oldPassword, new_password1: password1, new_password2: password2) |
|
|
|
|
let response: Token = try await self._runRequest(serviceCall: changePasswordCall, payload: params) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let params = ChangePasswordParams( |
|
|
|
|
old_password: oldPassword, new_password1: password1, new_password2: password2) |
|
|
|
|
let response: Token = try await self._runRequest( |
|
|
|
|
serviceCall: changePasswordCall, payload: params) |
|
|
|
|
|
|
|
|
|
self._storeToken(username: username, token: response.token) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The method send a request to reset the user's password |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - email: the email of the user |
|
|
|
|
public func forgotPassword(email: String) async throws { |
|
|
|
|
var postRequest = try self._baseRequest(servicePath: "dj-rest-auth/password/reset/", method: .post, requiresToken: false) |
|
|
|
|
var postRequest = try self._baseRequest( |
|
|
|
|
servicePath: "dj-rest-auth/password/reset/", method: .post, requiresToken: false) |
|
|
|
|
postRequest.httpBody = try jsonEncoder.encode(Email(email: email)) |
|
|
|
|
let response: Email = try await self._runRequest(postRequest) |
|
|
|
|
Logger.log("response = \(response)") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 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 deleteAccount() async throws { |
|
|
|
|
public func deleteAccount() async throws { |
|
|
|
|
guard let userId = StoreCenter.main.userId else { |
|
|
|
|
throw ServiceError.missingUserId |
|
|
|
|
} |
|
|
|
|
let path = "users/\(userId)/" |
|
|
|
|
let deleteAccount = ServiceCall(path: path, method: .delete, requiresToken: true) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let request = try self._baseRequest(call: deleteAccount) |
|
|
|
|
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 { |
|
|
|
|
@ -459,13 +604,15 @@ public class Services { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Parse a json data and tries to extract its error message |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - data: some JSON data |
|
|
|
|
fileprivate func errorMessageFromResponse(data: Data) -> ErrorMessage? { |
|
|
|
|
do { |
|
|
|
|
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { |
|
|
|
|
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) |
|
|
|
|
as? [String: Any] |
|
|
|
|
{ |
|
|
|
|
if let tuple = jsonObject.first { |
|
|
|
|
var error = "" |
|
|
|
|
if let stringsArray = tuple.value as? [String], let first = stringsArray.first { |
|
|
|
|
@ -481,17 +628,29 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func migrateToken(_ services: Services, userName: String) throws { |
|
|
|
|
try self._storeToken(username: userName, token: services.keychainStore.getValue()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//struct GetSyncLog: Codable { |
|
|
|
|
// var updates: [String: Codable] |
|
|
|
|
// var deletions: [String: Codable] |
|
|
|
|
//} |
|
|
|
|
|
|
|
|
|
struct SyncPayload<T: Encodable>: Encodable { |
|
|
|
|
var operation: String |
|
|
|
|
var modelName: String |
|
|
|
|
var data: T |
|
|
|
|
var storeId: String? |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct ErrorMessage { |
|
|
|
|
let error: String |
|
|
|
|
let domain: String |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var message: String { |
|
|
|
|
return "\(self.error) (\(self.domain))" |
|
|
|
|
} |
|
|
|
|
@ -512,7 +671,7 @@ struct Email: Codable { |
|
|
|
|
var email: String |
|
|
|
|
} |
|
|
|
|
struct Empty: Codable { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
struct Logout: Codable { |
|
|
|
|
var deviceId: String |
|
|
|
|
@ -525,7 +684,7 @@ public protocol UserBase: Codable { |
|
|
|
|
var id: String { get } |
|
|
|
|
var username: String { get } |
|
|
|
|
var email: String { get } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func uuid() throws -> UUID |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|