diff --git a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 07e3a91..2358118 100644 --- a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -24,5 +24,21 @@ stopOnStyle = "0"> + + + + diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index 8ce7358..2bd49ef 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -18,10 +18,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let config = Realm.Configuration( - schemaVersion: 3, - migrationBlock: { migration, oldSchemaVersion in - }) + let config = Realm.Configuration( + schemaVersion: 5, + migrationBlock: { migration, oldSchemaVersion in + if oldSchemaVersion <= 3 { + var numbers: [String] = [] + migration.enumerateObjects(ofType: "Vehicle") { old, new in + if let number = old?["number"] as? String { + if numbers.contains(number) { + migration.delete(old!) + } else { + numbers.append(number) + } + } + } + } + }) Realm.Configuration.defaultConfiguration = config diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift index 6e91275..3c87d72 100644 --- a/AutoCat/Controllers/CheckController.swift +++ b/AutoCat/Controllers/CheckController.swift @@ -6,7 +6,7 @@ import SwiftDate import RxRealm import RxDataSources -class CheckController: UIViewController, MaskedTextFieldDelegateListener { +class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITableViewDelegate { @IBOutlet weak var number: UITextField! @IBOutlet weak var check: UIButton! @@ -32,7 +32,7 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { } else { return UITableViewCell() } - }) + }, canEditRowAtIndexPath: { _, _ in true }) ds.titleForHeaderInSection = { dataSourse, index in return dataSourse.sectionModels[index].header @@ -42,11 +42,14 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { .subscribe(onNext: self.updateDetailController(with:)) .disposed(by: self.bag) + Observable.collection(from: realm.objects(Vehicle.self) .sorted(byKeyPath: "addedDate", ascending: false)) .map { $0.groupedByDate() } .bind(to: self.history.rx.items(dataSource: ds)) .disposed(by: self.bag) + + self.history.rx.setDelegate(self).disposed(by: self.bag) } override func viewWillAppear(_ animated: Bool) { @@ -111,7 +114,7 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { func onReceivedVehicle(_ vehicle: Vehicle) { if let realm = try? Realm() { try? realm.write { - realm.add(vehicle) + realm.add(vehicle, update: .modified) } } @@ -138,5 +141,21 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { } } - + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let vehicle: Vehicle = try? self.history.rx.model(at: indexPath) else { return nil } + + let updateAction = UIContextualAction(style: .normal, title: "Update") { action, view, completion in + IHProgressHUD.show() + Api.checkVehicle(by: vehicle.number, force: true) + .observeOn(MainScheduler.instance) + .subscribe(onNext: self.onReceivedVehicle(_:), onError: { err in + IHProgressHUD.showError(withStatus: err.localizedDescription) + print(err.localizedDescription) + }).disposed(by: self.bag) + completion(true) + } + updateAction.image = UIImage(systemName: "arrow.2.circlepath") + updateAction.backgroundColor = .systemBlue + return UISwipeActionsConfiguration(actions: [updateAction]) + } } diff --git a/AutoCat/Controllers/GoogleSignInController.swift b/AutoCat/Controllers/GoogleSignInController.swift index daec371..ba15d99 100644 --- a/AutoCat/Controllers/GoogleSignInController.swift +++ b/AutoCat/Controllers/GoogleSignInController.swift @@ -1,10 +1,11 @@ import UIKit import WebKit import CommonCrypto +import RxSwift -private struct TokenResponse: Codable { +struct TokenResponse: Codable { var id_token: String - var refresh_token: String + var refresh_token: String? var access_token: String var expires_in: Int var token_type: String @@ -14,6 +15,7 @@ private struct TokenResponse: Codable { class GoogleSignInController: UIViewController, WKNavigationDelegate { @IBOutlet weak var webView: WKWebView! + private var bag = DisposeBag() private var codeVerifier: String = "" public var completion: (() -> Void)? @@ -35,7 +37,7 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { + "&code_challenge_method=S256" + "&scope=email%20profile" + "&redirect_uri=" + Constants.googleRedirectURL - + "&client_id=" + Constants.googleClientId + + "&client_id=" + Constants.fbClientId + "&code_challenge=" + codeChallenge if let url = URL(string: authUrlString) { @@ -50,23 +52,15 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { if let queryItems = components.queryItems { if let code = queryItems.first(where: { $0.name == "code" })?.value { decisionHandler(.cancel) - self.getToken(code: code) { error, idToken, refreshToken in - if let err = error { - DispatchQueue.main.async { - self.dismiss(animated: true) { - IHProgressHUD.showError(withStatus: err.localizedDescription) - } - } - } else { - Settings.shared.user.googleIdToken = idToken - Settings.shared.user.googleRefreshToken = refreshToken - DispatchQueue.main.async { - self.dismiss(animated: true) { - self.completion?() - } - } - } - } + self.getToken(code: code) + .flatMap(fbVerifyAssertion) + .observeOn(MainScheduler.instance) + .subscribe(onNext: { _ in + self.dismiss(animated: true, completion: self.completion) + }, onError: { error in + IHProgressHUD.showError(withStatus: error.localizedDescription) + }) + .disposed(by: self.bag) return } } @@ -91,33 +85,65 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { return String(data: Data(Base64FS.encode(data: hash)), encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "=")) } - func getToken(code: String, completion: @escaping (Error?, String?, String?) -> Void) { + func getToken(code: String) -> Observable { let tokenUrlString = Constants.googleTokenURL + "?grant_type=authorization_code" + "&code=" + code + "&redirect_uri=" + Constants.googleRedirectURL - + "&client_id=" + Constants.googleClientId + + "&client_id=" + Constants.fbClientId + "&code_verifier=" + self.codeVerifier if let url = URL(string: tokenUrlString) { var request = URLRequest(url: url) request.httpMethod = "POST" - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { - if let resp = try? JSONDecoder().decode(TokenResponse.self, from: data) { - completion(nil, resp.id_token, resp.refresh_token) - } else { - let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Response parsing error"]) - completion(error, nil, nil) - } - } else { - completion(error, nil, nil) - } + return URLSession.shared.rx.data(request: request).map { data in + return try JSONDecoder().decode(TokenResponse.self, from: data) } - task.resume() } else { let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"]) - completion(error, nil, nil) + return Observable.error(error) } } + + public func fbVerifyAssertion(tokenResp: TokenResponse) -> Observable { + let signupUrl = Constants.fbVerifyAssertion + "?key=" + (Constants.fbApiKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? Constants.fbApiKey) + if let url = URL(string: signupUrl) { + let innerBody = "providerId=google.com" + + "&id_token=" + tokenResp.id_token + + "&access_token=" + tokenResp.access_token + + 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.vin01BUndleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") + request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") + + return URLSession.shared.rx.json(request: request).map { response in + guard let json = response as? [String: Any] else { return } + if let newToken = json["idToken"] as? String { + Settings.shared.user.googleIdToken = newToken + print("Token: \(newToken)") + } + if let newRefreshToken = json["refreshToken"] as? String { + Settings.shared.user.googleRefreshToken = newRefreshToken + print("Refresh token: \(newRefreshToken)") + } + }.catchError { err in + print(err) + return .just(()) + } + } else { + return .just(()) + } + } } diff --git a/AutoCat/Info.plist b/AutoCat/Info.plist index f0170f0..afefe6a 100644 --- a/AutoCat/Info.plist +++ b/AutoCat/Info.plist @@ -22,6 +22,8 @@ NSAppTransportSecurity + NSAllowsArbitraryLoads + NSExceptionDomains avto-nomer.ru diff --git a/AutoCat/Models/Vehicle.swift b/AutoCat/Models/Vehicle.swift index eb33882..7bd9084 100644 --- a/AutoCat/Models/Vehicle.swift +++ b/AutoCat/Models/Vehicle.swift @@ -41,7 +41,6 @@ class VehiclePhoto: Object, Decodable { } class Vehicle: Object, Decodable, IdentifiableType { - @objc dynamic var _id: String = "" @objc dynamic var brand: VehicleBrand? @objc dynamic var model: VehicleModel? @objc dynamic var color: String? @@ -59,10 +58,9 @@ class Vehicle: Object, Decodable, IdentifiableType { @objc dynamic var addedBy: String = "" let photos = List() - var identity: String { _id } + var identity: String { number } enum CodingKeys: String, CodingKey { - case _id case brand case model case color @@ -83,7 +81,6 @@ class Vehicle: Object, Decodable, IdentifiableType { required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self._id = try container.decode(String.self, forKey: ._id) self.brand = try container.decodeIfPresent(VehicleBrand.self, forKey: .brand) self.model = try container.decodeIfPresent(VehicleModel.self, forKey: .model) self.color = try container.decodeIfPresent(String.self, forKey: .color) @@ -111,7 +108,10 @@ class Vehicle: Object, Decodable, IdentifiableType { init(_ number: String) { self.number = number - self._id = UUID().uuidString self.addedDate = Date().timeIntervalSince1970*1000 } + + override static func primaryKey() -> String? { + return "number" + } } diff --git a/AutoCat/ThirdParty/Api.swift b/AutoCat/ThirdParty/Api.swift index fb25795..887187e 100644 --- a/AutoCat/ThirdParty/Api.swift +++ b/AutoCat/ThirdParty/Api.swift @@ -2,7 +2,7 @@ import Foundation import RxSwift class Api { - private static let baseUrl = "https://vps.aliencat.pro:8443/" + private static let baseUrl = "http://127.0.0.1:3000/" //"https://vps.aliencat.pro:8443/" private static func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error { return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion]) @@ -36,7 +36,7 @@ class Api { return URLSession.shared.rx.data(request: request).map { data in // let str = String(data: data, encoding: .utf8) // print("================================") -// print(str) +// print(str?.replacingOccurrences(of: "\\\"", with: "\"")) // print("================================") let resp = try JSONDecoder().decode(Response.self, from: data) if resp.success { @@ -47,6 +47,44 @@ class Api { } } + public static func refreshFbToken() -> Observable { + guard let token = Settings.shared.user.googleIdToken, let refreshToken = Settings.shared.user.googleRefreshToken, let jwt = JWT(string: token), jwt.expired else { + return .just(()) + } + + let refreshUrlString = Constants.fbRefreshToken + "?key=" + Constants.fbApiKey + + let body = [ + "grantType": "refresh_token", + "refreshToken": refreshToken + ] + + if let url = URL(string: refreshUrlString) { + 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.vin01BUndleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") + request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") + return URLSession.shared.rx.json(request: request).map { resp in + guard let json = resp as? [String: Any] else { return } + if let newToken = json["id_token"] as? String { + Settings.shared.user.googleIdToken = newToken + print("Token was successfully refresh to: \(newToken)") + } + if let newRefreshToken = json["refresh_token"] as? String { + Settings.shared.user.googleRefreshToken = newRefreshToken + } + }.catchError { error in + print(error) + return .just(()) + } + } else { + return .just(()) + } + } + public static func login(username: String, password: String) -> Observable { let body = [ "login": username, @@ -71,14 +109,19 @@ class Api { return self.makeRequest(api: "vehicles", method: "GET", body: body) } - public static func checkVehicle(by number: String) -> Observable { - if let token = Settings.shared.user.googleIdToken, let jwt = JWT(string: token) { - - } - - return self.makeRequest(api: "vehicles/check", method: "POST", body: ["number": number]).map { (vehicle: Vehicle) -> Vehicle in - vehicle.addedDate = Date().timeIntervalSince1970*1000 - return vehicle + public static func checkVehicle(by number: String, force: Bool = false) -> Observable { + return self.refreshFbToken().flatMap { () -> Observable in + var body = [ + "number": number, + "forceUpdate": String(force) + ] + if let token = Settings.shared.user.googleIdToken { + body["googleIdToken"] = token + } + return self.makeRequest(api: "vehicles/check", method: "POST", body: body).map { (vehicle: Vehicle) -> Vehicle in + vehicle.addedDate = Date().timeIntervalSince1970*1000 + return vehicle + } } } } diff --git a/AutoCat/Utils/Constants.swift b/AutoCat/Utils/Constants.swift index 5672f45..86a21e5 100644 --- a/AutoCat/Utils/Constants.swift +++ b/AutoCat/Utils/Constants.swift @@ -5,6 +5,13 @@ enum Constants { static let googleTokenURL = "https://oauth2.googleapis.com/token" static let googleRedirectURL = "com.googleusercontent.apps.994679674451-k7clunkk4nicl6iuajdtc5u7hvustbdb:/oauth2callback" - static let googleClientId = "994679674451-k7clunkk4nicl6iuajdtc5u7hvustbdb.apps.googleusercontent.com" - static let googleApiKey = "AIzaSyDVlrQj_05y6AeZNf8enpSWFIiHhgwfnGI" + static let fbClientId = "994679674451-k7clunkk4nicl6iuajdtc5u7hvustbdb.apps.googleusercontent.com" + static let fbApiKey = "AIzaSyDVlrQj_05y6AeZNf8enpSWFIiHhgwfnGI" + + static let fbClientVersion = "iOS/FirebaseSDK/6.5.1/FirebaseCore-iOS" + static let fbVerifyAssertion = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion" + static let fbRefreshToken = "https://securetoken.googleapis.com/v1/token" + static let fbUserAgent = "FirebaseAuth.iOS/6.5.1 ru.Vin01/1.0 iPhone/13.5 hw/sim" + + static let vin01BUndleId = "ru.Vin01" }