import Foundation public actor ApiService: ApiServiceProtocol { let settingsService: SettingsServiceProtocol public init(settingsService: SettingsServiceProtocol) { self.settingsService = settingsService } private let session: URLSession = { let sessionConfig = URLSessionConfiguration.default sessionConfig.timeoutIntervalForRequest = 40.0 sessionConfig.timeoutIntervalForResource = 40.0 return URLSession(configuration: sessionConfig) }() // MARK: - Private wrappres private func createRequest(api: String, method: String, body: B? = nil, params: [String:P]? = nil) async -> URLRequest? where B: Encodable, P: LosslessStringConvertible { let baseUrl = await settingsService.backend.baseUrl guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil } if let params = params, method.uppercased() == "GET" { urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: String($1)) } } var request = URLRequest(url: urlComponents.url!) request.httpMethod = method request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") request.addValue("Bearer " + (await settingsService.user.token), forHTTPHeaderField: "Authorization") if let body = body, method.uppercased() != "GET" { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted if let data = try? encoder.encode(body) { request.httpBody = data } } return request } private func makeRequest(api: String, method: String = "GET", body: B?, params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible { guard let request = await self.createRequest(api: api, method: method, body: body, params: params) else { throw ApiError.generic } let (data, resp) = try await session.data(for: request) guard data.count > 0 else { throw ApiError.emptyResponse } guard let httpResp = resp as? HTTPURLResponse else { throw ApiError.generic } if httpResp.statusCode == 401 { throw ApiError.unauthorized } if httpResp.statusCode < 200 || httpResp.statusCode >= 300 { throw ApiError.httpError(httpResp.statusCode) } do { let resp = try JSONDecoder().decode(Response.self, from: data) if resp.success { return resp.data! } else { throw ApiError.message(resp.error!) } } catch let error as Swift.DecodingError { throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription) } catch { throw error } } private func makeGetRequest(api: String, params:[String: P]? = nil) async throws -> T where T: Decodable, P: LosslessStringConvertible { // Kind of hack to satisfy compiler try await makeRequest(api: api, method: "GET", body: nil as Int?, params: params) } private func makeEmptyGetRequest(api: String) async throws -> T where T: Decodable { try await makeRequest(api: api, method: "GET", body: nil as Int?, params: nil as [String:Int]?) } private func makeEmptyBodyRequest(api: String, method: String = "POST") async throws -> T where T: Decodable { try await makeRequest(api: api, method: method, body: nil as Int?, params: nil as [String:Int]?) } private func makeBodyRequest(api: String, body: B?, method: String = "POST") async throws -> T where T: Decodable, B: Encodable { try await makeRequest(api: api, method: method, body: body, params: nil as [String:Int]?) } // MARK: - Firebase API public func refreshFbToken() async throws { guard let token = await settingsService.user.firebaseIdToken, let refreshToken = await settingsService.user.firebaseRefreshToken, let jwt = JWT(string: token), jwt.expired else { return } let refreshUrlString = Constants.fbRefreshToken + "?key=" + Constants.fbApiKey guard let url = URL(string: refreshUrlString) else { return } let body = [ "grantType": "refresh_token", "refreshToken": refreshToken ] var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") let (data, _) = try await session.data(for: request) let model = try JSONDecoder().decode(FbRefreshTokenModel.self, from: data) if let idToken = model.id_token { await settingsService.setFirebaseIdToken(idToken) } if let refreshToken = model.refresh_token { await settingsService.setFirebaseRefreshToken(refreshToken) } } public func fbVerifyAssertion(provider: String, idToken: String, accessToken: String? = nil) async { let signupUrl = Constants.fbVerifyAssertion + "?key=" + (Constants.fbApiKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? Constants.fbApiKey) guard let url = URL(string: signupUrl) else { return } var innerBody = "providerId=" + provider + "&id_token=" + idToken if let accessToken = accessToken { innerBody += "&access_token=" + accessToken } let body: [String:Any] = [ "returnIdpCredential": true, "returnSecureToken": true, "autoCreate": true, "requestUri": "http://localhost", "postBody": innerBody ] var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") do { let (data, _) = try await session.data(for: request) let model = try JSONDecoder().decode(FbVerifyTokenModel.self, from: data) if let idToken = model.idToken { await settingsService.setFirebaseIdToken(idToken) } if let refreshToken = model.refreshToken { await settingsService.setFirebaseRefreshToken(refreshToken) } } catch { print("Firebase verify token error: \(error.localizedDescription)") } } public func getToken(code: String, codeVerifier: String) async throws -> TokenResponse { let tokenUrlString = Constants.googleTokenURL + "?grant_type=authorization_code" + "&code=" + code + "&redirect_uri=" + Constants.googleRedirectURL + "&client_id=" + Constants.fbClientId + "&code_verifier=" + codeVerifier if let url = URL(string: tokenUrlString) { var request = URLRequest(url: url) request.httpMethod = "POST" let (data, _) = try await URLSession.shared.data(for: request) return try JSONDecoder().decode(TokenResponse.self, from: data) } else { throw ApiError.badUrl } } // MARK: - AutoCat public API public func login(email: String, password: String) async throws -> User { let body = [ "email": email, "password": password ] return try await makeBodyRequest(api: "user/login", body: body) } public func signIn(email: String, password: String) async throws -> User { let body = [ "email": email, "password": password ] return try await makeBodyRequest(api: "user/signIn", body: body) } public func signUp(email: String, password: String) async throws -> User { let body = [ "email": email, "password": password ] return try await makeBodyRequest(api: "user/signup", body: body) } public func getVehicles(with filter: Filter, pageToken: String? = nil, pageSize: Int = 50) async throws -> PagedResponse { var params = filter.queryDictionary() params["pageSize"] = String(pageSize) if let token = pageToken { params["pageToken"] = token; } return try await makeGetRequest(api: "vehicles", params: params) } public func checkVehicle( by number: String, numberType: VehicleNumberType, notes: [VehicleNoteDto], events: [VehicleEventDto], force: Bool = false ) async throws -> VehicleDto { try await refreshFbToken() var body = [ "number": AnyEncodable(number), "forceUpdate": AnyEncodable(force), "type": AnyEncodable(numberType.value) ] if let token = await settingsService.user.firebaseIdToken { body["googleIdToken"] = AnyEncodable(token) } if !notes.isEmpty { body["notes"] = AnyEncodable(notes) } if !events.isEmpty { body["events"] = AnyEncodable(events) } return try await makeBodyRequest(api: "vehicles/check", body: body) } public func getReport(for number: String) async throws -> VehicleDto { try await makeGetRequest(api: "vehicles/report", params: ["number": number]) } public func getBrands() async throws -> [String] { try await makeEmptyGetRequest(api: "vehicles/brands") } public func getModels(of brand: String) async throws -> [String] { try await makeGetRequest(api: "vehicles/models", params: ["brand": brand]) } public func getColors() async throws -> [String] { try await makeEmptyGetRequest(api: "vehicles/colors") } public func getRegions() async throws -> [VehicleRegion] { try await makeEmptyGetRequest(api: "vehicles/regions") } public func getYears() async throws -> [Int] { try await makeEmptyGetRequest(api: "vehicles/years") } public func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto { let body = ["number": AnyEncodable(number), "event": AnyEncodable(event)] return try await makeBodyRequest(api: "events", body: body) } public func remove(event id: String) async throws -> VehicleDto { let body = ["eventId": id] return try await makeBodyRequest(api: "events", body: body, method: "DELETE") } public func edit(event: VehicleEventDto) async throws -> VehicleDto { let body = ["event": event] return try await makeBodyRequest(api: "events", body: body, method: "PUT") } public func events(with filter: Filter) async throws -> [VehicleEventDto] { try await makeGetRequest(api: "events", params: filter.queryDictionary()) } public func checkOsago(number: String?, vin: String?, date: Date, token: String) async throws -> VehicleDto { let body = [ "date": AnyEncodable(date.timeIntervalSince1970), "number": AnyEncodable(number), "vin": AnyEncodable(vin), "token": AnyEncodable(token) ] return try await makeBodyRequest(api: "vehicles/checkOsago", body: body) } public func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto { let body = ["number": AnyEncodable(number), "notes": AnyEncodable(notes)] return try await makeBodyRequest(api: "notes", body: body) } public func edit(note: VehicleNoteDto) async throws -> VehicleDto { try await makeBodyRequest(api: "notes", body: ["note": note], method: "PUT") } public func remove(note id: String) async throws -> VehicleDto { try await makeBodyRequest(api: "notes", body: ["noteId": id], method: "DELETE") } public func checkVehicleGb(by number: String) async throws -> VehicleDto { try await refreshFbToken() var body = ["number": number] if let token = await settingsService.user.firebaseIdToken { body["token"] = token } return try await makeBodyRequest(api: "vehicles/checkGbTg", body: body) } }