398 lines
15 KiB
Swift
398 lines
15 KiB
Swift
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<B,P>(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<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 = 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<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 = await settingsService.user.firebaseIdToken,
|
|
let refreshToken = await settingsService.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 {
|
|
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<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,
|
|
numberType: VehicleNumberType,
|
|
notes: [VehicleNoteDto],
|
|
events: [VehicleEventDto],
|
|
avtocodReport: String?,
|
|
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)
|
|
}
|
|
|
|
if let avtocodReport {
|
|
body["avtocodReport"] = AnyEncodable(avtocodReport)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
public func getAvtocodHtml(by number: String, numberType: VehicleNumberType) async throws -> String? {
|
|
guard var urlComponents = URLComponents(string: Constants.avtocodBaseURL + "auto/generate") else {
|
|
return nil
|
|
}
|
|
|
|
urlComponents.queryItems = [
|
|
URLQueryItem(name: "number", value: number),
|
|
URLQueryItem(name: "type", value: numberType.value),
|
|
URLQueryItem(name: "device_token", value: UUID().uuidString)
|
|
]
|
|
|
|
guard let url = urlComponents.url else {
|
|
return nil
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.addValue(Constants.avtocodUserAgent, forHTTPHeaderField: "User-Agent")
|
|
|
|
let (data, _) = try await session.data(for: request)
|
|
let json = try JSONDecoder().decode(AvtocodResponse.self, from: data)
|
|
|
|
guard let reportUrl = json.report_uri.flatMap(URL.init(string:)) else {
|
|
return nil
|
|
}
|
|
|
|
request = URLRequest(url: reportUrl)
|
|
request.addValue(Constants.avtocodUserAgent, forHTTPHeaderField: "User-Agent")
|
|
let (reportData, _) = try await session.data(for: request)
|
|
let reportBase64 = reportData.base64EncodedString()
|
|
|
|
return reportBase64
|
|
}
|
|
}
|