|
|
|
|
@ -18,6 +18,38 @@ enum ServiceError: Error { |
|
|
|
|
case urlCreationError(url: String) |
|
|
|
|
case cantConvertToUUID(id: String) |
|
|
|
|
case missingUserName |
|
|
|
|
case responseError(response: String) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate enum ServiceConf: String { |
|
|
|
|
case createAccount = "users/" |
|
|
|
|
case requestToken = "plus/api-token-auth/" |
|
|
|
|
case getUser = "plus/user-by-token/" |
|
|
|
|
case changePassword = "plus/change-password/" |
|
|
|
|
|
|
|
|
|
var method: Method { |
|
|
|
|
switch self { |
|
|
|
|
case .createAccount, .requestToken: |
|
|
|
|
return .post |
|
|
|
|
case .changePassword: |
|
|
|
|
return .put |
|
|
|
|
default: |
|
|
|
|
return .get |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var requiresToken: Bool? { |
|
|
|
|
switch self { |
|
|
|
|
case .createAccount, .requestToken: |
|
|
|
|
return false |
|
|
|
|
case .getUser, .changePassword: |
|
|
|
|
return true |
|
|
|
|
default: |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public class Services { |
|
|
|
|
@ -49,15 +81,23 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// MARK: - Base |
|
|
|
|
|
|
|
|
|
fileprivate func _runRequest<T : Encodable, U : Decodable>(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U { |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
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) |
|
|
|
|
Logger.log("response = \(String(describing: String(data: task.0, encoding: .utf8)))") |
|
|
|
|
|
|
|
|
|
if let response = task.1 as? HTTPURLResponse { |
|
|
|
|
let statusCode = response.statusCode |
|
|
|
|
Logger.log("request ended with status code = \(statusCode)") |
|
|
|
|
@ -71,10 +111,10 @@ public class Services { |
|
|
|
|
if let apiCallId, let type = (T.self as? any Storable.Type) { |
|
|
|
|
try Store.main.rescheduleApiCall(id: apiCallId, type: type) |
|
|
|
|
} |
|
|
|
|
let dataString = String(describing: String(data: task.0, encoding: .utf8)) |
|
|
|
|
throw ServiceError.responseError(response: dataString) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Logger.log("response = \(String(describing: String(data: task.0, encoding: .utf8)))") |
|
|
|
|
return try jsonDecoder.decode(T.self, from: task.0) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -82,7 +122,11 @@ public class Services { |
|
|
|
|
return try self._baseRequest(servicePath: servicePath, method: .get) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _baseRequest(servicePath: String, method: Method) throws -> URLRequest { |
|
|
|
|
fileprivate func _baseRequest(conf: ServiceConf) throws -> URLRequest { |
|
|
|
|
return try self._baseRequest(servicePath: conf.rawValue, method: conf.method, requiresToken: conf.requiresToken) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _baseRequest(servicePath: String, method: Method, requiresToken: Bool? = nil) throws -> URLRequest { |
|
|
|
|
let urlString = baseURL + servicePath |
|
|
|
|
guard let url = URL(string: urlString) else { |
|
|
|
|
throw ServiceError.urlCreationError(url: urlString) |
|
|
|
|
@ -90,7 +134,8 @@ 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() { |
|
|
|
|
if !(requiresToken == false), let token = try? self.keychainStore.getToken() { |
|
|
|
|
Logger.log("current token = \(token)") |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -99,17 +144,17 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// MARK: - Services |
|
|
|
|
|
|
|
|
|
func get<T : Storable>() async throws -> [T] { |
|
|
|
|
func get<T: Storable>() async throws -> [T] { |
|
|
|
|
let getRequest = try getRequest(servicePath: T.resourceName() + "/") |
|
|
|
|
return try await self._runRequest(getRequest) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T { |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _request<T : Storable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
fileprivate func _request<T: Storable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
guard let url = URL(string: apiCall.url) else { |
|
|
|
|
throw ServiceError.urlCreationError(url: apiCall.url) |
|
|
|
|
} |
|
|
|
|
@ -130,9 +175,9 @@ public class Services { |
|
|
|
|
|
|
|
|
|
// MARK: - Authentication |
|
|
|
|
|
|
|
|
|
public func createAccount<U : UserPasswordBase, V : UserBase>(user: U) async throws -> V { |
|
|
|
|
public func createAccount<U: UserPasswordBase, V: UserBase>(user: U) async throws -> V { |
|
|
|
|
|
|
|
|
|
let response: V = try await _runRequest(servicePath: "users/", method: .post, payload: user) |
|
|
|
|
let response: V = try await _runRequest(serviceConf: .createAccount, payload: user) |
|
|
|
|
|
|
|
|
|
// var postRequest = try self._baseRequest(servicePath: "users/", method: .post) |
|
|
|
|
// postRequest.httpBody = try jsonEncoder.encode(user) |
|
|
|
|
@ -143,7 +188,7 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func requestToken(username: String, password: String) async throws -> String { |
|
|
|
|
var postRequest = try self._baseRequest(servicePath: "plus/api-token-auth/", method: .post) |
|
|
|
|
var postRequest = try self._baseRequest(conf: .requestToken) |
|
|
|
|
let credentials = Credentials(username: username, password: password) |
|
|
|
|
postRequest.httpBody = try jsonEncoder.encode(credentials) |
|
|
|
|
let response: AuthResponse = try await self._runRequest(postRequest) |
|
|
|
|
@ -153,42 +198,37 @@ public class Services { |
|
|
|
|
|
|
|
|
|
fileprivate func _storeToken(username: String, token: String) { |
|
|
|
|
do { |
|
|
|
|
try self.keychainStore.deleteToken() |
|
|
|
|
try self.keychainStore.add(username: username, token: token) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
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) |
|
|
|
|
let user: U = try await self._runRequest(postRequest) |
|
|
|
|
Logger.log("user = \(user.username), id = \(user.id)") |
|
|
|
|
Store.main.setUserUUID(uuidString: user.id) |
|
|
|
|
Store.main.setUserName(user.username) |
|
|
|
|
return user |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func changePassword(password1: String, password2: String) async throws { |
|
|
|
|
public func changePassword(oldPassword: String, password1: String, password2: String) async throws { |
|
|
|
|
|
|
|
|
|
guard let username = Store.main.userName() else { |
|
|
|
|
throw ServiceError.missingUserName |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct ChangePasswordParams: Codable { |
|
|
|
|
var old_password: String |
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
let params = ChangePasswordParams(old_password: oldPassword, new_password1: password1, new_password2: password2) |
|
|
|
|
let response: Token = try await self._runRequest(serviceConf: .changePassword, payload: params) |
|
|
|
|
|
|
|
|
|
self._storeToken(username: username, token: response.token) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -220,7 +260,7 @@ struct Token: Codable { |
|
|
|
|
var token: String |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public protocol UserBase : Codable { |
|
|
|
|
public protocol UserBase: Codable { |
|
|
|
|
var id: String { get } |
|
|
|
|
var username: String { get } |
|
|
|
|
// var password: String? { get } |
|
|
|
|
|