Firebase authorization

This commit is contained in:
Selim Mustafaev 2020-06-01 11:41:37 +03:00
parent 5fd6bf65f3
commit 523fb5d411
8 changed files with 185 additions and 60 deletions

View File

@ -24,5 +24,21 @@
stopOnStyle = "0">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "143FAB5A-C171-426A-A0BA-67C44E72B9D5"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "AutoCat/Controllers/GoogleSignInController.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "61"
endingLineNumber = "61"
landmarkName = "webView(_:decidePolicyFor:decisionHandler:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -19,8 +19,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 3,
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

View File

@ -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])
}
}

View File

@ -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<TokenResponse> {
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)
return URLSession.shared.rx.data(request: request).map { data in
return try JSONDecoder().decode(TokenResponse.self, from: data)
}
} else {
completion(error, nil, nil)
}
}
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<Void> {
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(())
}
}
}

View File

@ -22,6 +22,8 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>avto-nomer.ru</key>

View File

@ -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<VehiclePhoto>()
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"
}
}

View File

@ -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<T>.self, from: data)
if resp.success {
@ -47,6 +47,44 @@ class Api {
}
}
public static func refreshFbToken() -> Observable<Void> {
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<User> {
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<Vehicle> {
if let token = Settings.shared.user.googleIdToken, let jwt = JWT(string: token) {
public static func checkVehicle(by number: String, force: Bool = false) -> Observable<Vehicle> {
return self.refreshFbToken().flatMap { () -> Observable<Vehicle> 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: ["number": number]).map { (vehicle: Vehicle) -> Vehicle in
return self.makeRequest(api: "vehicles/check", method: "POST", body: body).map { (vehicle: Vehicle) -> Vehicle in
vehicle.addedDate = Date().timeIntervalSince1970*1000
return vehicle
}
}
}
}

View File

@ -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"
}