AutoCat/AutoCatCore/Services/ApiService/ApiService.swift

335 lines
13 KiB
Swift

import Foundation
public actor ApiService: ApiServiceProtocol {
public static let shared = ApiService(settingsService: SettingsService.shared)
let settingsService: SettingsServiceProtocol
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<B,P>(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible {
let baseUrl = settingsService.useTestBackend ? Constants.baseUrlDebug : Constants.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 " + Settings.shared.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<T,B,P>(api: String,
method: String = "GET",
body: B?,
params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible {
guard let request = 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<T>.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<T,P>(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<T>(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<T>(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<T,B>(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 = Settings.shared.user.firebaseIdToken,
let refreshToken = Settings.shared.user.firebaseRefreshToken,
let jwt = JWT<FirebasePayload>(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 {
Settings.shared.user.firebaseIdToken = idToken
}
if let refreshToken = model.refresh_token {
Settings.shared.user.firebaseRefreshToken = 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 {
Settings.shared.user.firebaseIdToken = idToken
}
if let refreshToken = model.refreshToken {
Settings.shared.user.firebaseRefreshToken = refreshToken
}
} catch {
print("Firebase verify token error: \(error.localizedDescription)")
}
}
// 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<VehicleDto> {
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, notes: [VehicleNoteDto], events: [VehicleEventDto], force: Bool = false) async throws -> VehicleDto {
try await refreshFbToken()
var body = [
"number": AnyEncodable(number),
"forceUpdate": AnyEncodable(force)
]
if let token = Settings.shared.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 = Settings.shared.user.firebaseIdToken {
body["token"] = token
}
return try await makeBodyRequest(api: "vehicles/checkGbTg", body: body)
}
}