You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
12 KiB
324 lines
12 KiB
//
|
|
// Services.swift
|
|
// LeStorage
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public enum HTTPMethod: String, CaseIterable {
|
|
case get = "GET"
|
|
case post = "POST"
|
|
case put = "PUT"
|
|
case delete = "DELETE"
|
|
}
|
|
|
|
public enum ServiceError: Error {
|
|
case urlCreationError(url: String)
|
|
case cantConvertToUUID(id: String)
|
|
case missingUserName
|
|
case responseError(response: String)
|
|
case missingToken
|
|
}
|
|
|
|
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: HTTPMethod {
|
|
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 {
|
|
|
|
let keychainStore: KeychainStore
|
|
|
|
public init(url: String) {
|
|
self.baseURL = url
|
|
self.keychainStore = KeychainStore(serverId: url)
|
|
}
|
|
|
|
fileprivate(set) var baseURL: String
|
|
|
|
fileprivate var jsonEncoder: JSONEncoder = {
|
|
let encoder = JSONEncoder()
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
encoder.outputFormatting = .prettyPrinted
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
return encoder
|
|
}()
|
|
|
|
fileprivate var jsonDecoder: JSONDecoder = {
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
return decoder
|
|
}()
|
|
|
|
// MARK: - Base
|
|
|
|
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: HTTPMethod, 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)
|
|
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)")
|
|
switch statusCode {
|
|
case 200..<300:
|
|
if let apiCallId,
|
|
let collectionName = (T.self as? any Storable.Type)?.resourceName() {
|
|
try await MainActor.run {
|
|
try Store.main.deleteApiCallById(apiCallId, collectionName: collectionName)
|
|
}
|
|
}
|
|
default:
|
|
if let apiCallId, let type = (T.self as? any Storable.Type) {
|
|
try Store.main.rescheduleApiCall(id: apiCallId, type: type)
|
|
}
|
|
Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")")
|
|
var dataString = String(describing: String(data: task.0, encoding: .utf8))
|
|
if let nfe: NonFieldError = try? JSONDecoder().decode(NonFieldError.self, from: task.0) {
|
|
if let reason = nfe.non_field_errors.first {
|
|
dataString = reason
|
|
}
|
|
}
|
|
throw ServiceError.responseError(response: dataString)
|
|
}
|
|
}
|
|
return try jsonDecoder.decode(T.self, from: task.0)
|
|
}
|
|
|
|
fileprivate func _isTokenRequired<T : Storable>(type: T.Type, method: HTTPMethod) -> Bool {
|
|
let methods = T.tokenExemptedMethods()
|
|
if methods.contains(method) {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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: HTTPMethod, requiresToken: Bool? = nil) throws -> URLRequest {
|
|
let urlString = baseURL + servicePath
|
|
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")
|
|
if !(requiresToken == false) {
|
|
// Logger.log("current token = \(token)")
|
|
do {
|
|
let token = try self.keychainStore.getToken()
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
|
|
} catch {
|
|
throw ServiceError.missingToken
|
|
}
|
|
}
|
|
|
|
return request
|
|
}
|
|
|
|
// MARK: - Services
|
|
|
|
public func get<T: Storable>() async throws -> [T] {
|
|
let getRequest = try _getRequest(type: T.self)
|
|
return try await self._runRequest(getRequest)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
guard let url = URL(string: apiCall.url) else {
|
|
throw ServiceError.urlCreationError(url: apiCall.url)
|
|
}
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = apiCall.method
|
|
request.httpBody = apiCall.body.data(using: .utf8)
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
if let method = HTTPMethod(rawValue: apiCall.method), self._isTokenRequired(type: T.self, method: method) {
|
|
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<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
|
|
}
|
|
|
|
public func requestToken(username: String, password: String) async throws -> String {
|
|
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)
|
|
self._storeToken(username: username, token: response.token)
|
|
return response.token
|
|
}
|
|
|
|
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 {
|
|
_ = try await requestToken(username: username, password: password)
|
|
let postRequest = try self._baseRequest(conf: .getUser)
|
|
let user: U = try await self._runRequest(postRequest)
|
|
Store.main.setUserUUID(uuidString: user.id)
|
|
Store.main.setUserName(user.username)
|
|
return user
|
|
}
|
|
|
|
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(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)
|
|
}
|
|
|
|
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))
|
|
let response: Email = try await self._runRequest(postRequest)
|
|
Logger.log("response = \(response)")
|
|
}
|
|
|
|
func disconnect() throws {
|
|
try self.keychainStore.deleteToken()
|
|
}
|
|
|
|
}
|
|
|
|
struct AuthResponse: Codable {
|
|
let token: String
|
|
}
|
|
|
|
struct Credentials: Codable {
|
|
var username: String
|
|
var password: String
|
|
}
|
|
struct Token: Codable {
|
|
var token: String
|
|
}
|
|
struct Email: Codable {
|
|
var email: String
|
|
}
|
|
struct NonFieldError: Codable {
|
|
var non_field_errors: [String]
|
|
}
|
|
|
|
public protocol UserBase: Codable {
|
|
var id: String { get }
|
|
var username: String { get }
|
|
var email: String { get }
|
|
|
|
func uuid() throws -> UUID
|
|
}
|
|
|
|
public protocol UserPasswordBase: UserBase {
|
|
var password: String { get }
|
|
}
|
|
|