|
|
|
|
@ -19,7 +19,6 @@ public enum ServiceError: Error { |
|
|
|
|
case cantConvertToUUID(id: String) |
|
|
|
|
case missingUserName |
|
|
|
|
case responseError(response: String) |
|
|
|
|
// case missingToken |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate enum ServiceConf: String { |
|
|
|
|
@ -52,8 +51,10 @@ fileprivate enum ServiceConf: String { |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 |
|
|
|
|
|
|
|
|
|
public init(url: String) { |
|
|
|
|
@ -81,12 +82,21 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// MARK: - Base |
|
|
|
|
|
|
|
|
|
/// Runs a request using a configuration object |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - 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>(serviceConf: ServiceConf, payload: T, apiCallId: String? = nil) async throws -> U { |
|
|
|
|
var request = try self._baseRequest(conf: serviceConf) |
|
|
|
|
request.httpBody = try jsonEncoder.encode(payload) |
|
|
|
|
return try await _runRequest(request, apiCallId: apiCallId) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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: 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) |
|
|
|
|
@ -110,7 +120,6 @@ public class Services { |
|
|
|
|
errorString = message |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let apiCallId, let type = (T.self as? any Storable.Type) { |
|
|
|
|
try Store.main.rescheduleApiCall(id: apiCallId, type: type) |
|
|
|
|
Store.main.logFailedAPICall(apiCallId, collectionName: type.resourceName(), error: errorString) |
|
|
|
|
@ -122,6 +131,10 @@ public class Services { |
|
|
|
|
return try jsonDecoder.decode(T.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 { |
|
|
|
|
let methods = T.tokenExemptedMethods() |
|
|
|
|
if methods.contains(method) { |
|
|
|
|
@ -131,30 +144,50 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns a GET request for the resource |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the type of the request resource |
|
|
|
|
fileprivate func _getRequest<T: Storable>(type: T.Type) throws -> URLRequest { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .get) |
|
|
|
|
return try self._baseRequest(servicePath: T.path(), method: .get, requiresToken: requiresToken) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .post) |
|
|
|
|
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 { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .put) |
|
|
|
|
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 { |
|
|
|
|
let requiresToken = self._isTokenRequired(type: T.self, method: .delete) |
|
|
|
|
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(conf: ServiceConf) throws -> URLRequest { |
|
|
|
|
return try self._baseRequest(servicePath: conf.rawValue, method: conf.method, requiresToken: conf.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 |
|
|
|
|
fileprivate func _baseRequest(servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil) throws -> URLRequest { |
|
|
|
|
let urlString = baseURL + servicePath |
|
|
|
|
guard let url = URL(string: urlString) else { |
|
|
|
|
@ -173,28 +206,35 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// MARK: - Services |
|
|
|
|
|
|
|
|
|
/// Executes a GET request |
|
|
|
|
public func get<T: Storable>() async throws -> [T] { |
|
|
|
|
let getRequest = try _getRequest(type: T.self) |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Executes an ApiCall |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 { |
|
|
|
|
let url = try self._url(from: apiCall) |
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
|
@ -214,6 +254,9 @@ public class Services { |
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URL corresponding to the ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: an instance of ApiCall to build to URL |
|
|
|
|
fileprivate func _url<T: Storable>(from apiCall: ApiCall<T>) throws -> URL { |
|
|
|
|
var stringURL: String = self.baseURL |
|
|
|
|
switch apiCall.method { |
|
|
|
|
@ -231,14 +274,19 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// 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 { |
|
|
|
|
|
|
|
|
|
let response: V = try await _runRequest(serviceConf: .createAccount, payload: user) |
|
|
|
|
// let _ = try await requestToken(username: user.username, password: user.password) |
|
|
|
|
Store.main.setUserName(user.username) |
|
|
|
|
return response |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Requests a token for a username and password |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - username: the account's username |
|
|
|
|
/// - password: the account's password |
|
|
|
|
public func requestToken(username: String, password: String) async throws -> String { |
|
|
|
|
var postRequest = try self._baseRequest(conf: .requestToken) |
|
|
|
|
let credentials = Credentials(username: username, password: password) |
|
|
|
|
@ -248,6 +296,10 @@ public class Services { |
|
|
|
|
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.deleteToken() |
|
|
|
|
@ -257,6 +309,10 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 { |
|
|
|
|
_ = try await requestToken(username: username, password: password) |
|
|
|
|
let postRequest = try self._baseRequest(conf: .getUser) |
|
|
|
|
@ -266,6 +322,11 @@ public class Services { |
|
|
|
|
return user |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 { |
|
|
|
|
|
|
|
|
|
guard let username = Store.main.userName() else { |
|
|
|
|
@ -284,6 +345,9 @@ public class Services { |
|
|
|
|
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) |
|
|
|
|
postRequest.httpBody = try jsonEncoder.encode(Email(email: email)) |
|
|
|
|
@ -291,17 +355,21 @@ public class Services { |
|
|
|
|
Logger.log("response = \(response)") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func disconnect() throws { |
|
|
|
|
/// Deletes the locally stored token |
|
|
|
|
func deleteToken() throws { |
|
|
|
|
try self.keychainStore.deleteToken() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Parse a json data and tries to extract its error message |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - data: some JSON data |
|
|
|
|
fileprivate func errorMessageFromResponse(data: Data) -> String? { |
|
|
|
|
do { |
|
|
|
|
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let stringsArray = jsonObject.values.first as? [String] { |
|
|
|
|
return stringsArray.first |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
print("Failed to parse JSON: \(error.localizedDescription)") |
|
|
|
|
Logger.log("Failed to parse JSON: \(error.localizedDescription)") |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
@ -311,7 +379,6 @@ public class Services { |
|
|
|
|
struct AuthResponse: Codable { |
|
|
|
|
let token: String |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct Credentials: Codable { |
|
|
|
|
var username: String |
|
|
|
|
var password: String |
|
|
|
|
|