Skip to content

Commit

Permalink
implemented auth with personal access token
Browse files Browse the repository at this point in the history
  • Loading branch information
khoren93 committed May 17, 2020
1 parent 7a26308 commit 34d3627
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 47 deletions.
2 changes: 1 addition & 1 deletion SwiftHub/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class Application: NSObject {

if let token = authManager.token, Configs.Network.useStaging == false {
switch token.type() {
case .oAuth(let token):
case .oAuth(let token), .personal(let token):
provider = GraphApi(restApi: restApi, token: token)
default: break
}
Expand Down
1 change: 1 addition & 0 deletions SwiftHub/Configs/Configs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct Configs {

struct App {
static let githubUrl = "https://github.com/khoren93/SwiftHub"
static let githubScope = "user+repo+notifications+read:org"
static let bundleIdentifier = "com.public.SwiftHub"
}

Expand Down
13 changes: 13 additions & 0 deletions SwiftHub/Models/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import ObjectMapper

enum TokenType {
case basic(token: String)
case personal(token: String)
case oAuth(token: String)
case unauthorized

var description: String {
switch self {
case .basic: return "basic"
case .personal: return "personal"
case .oAuth: return "OAuth"
case .unauthorized: return "unauthorized"
}
Expand All @@ -30,6 +32,9 @@ struct Token: Mappable {
// Basic
var basicToken: String?

// Personal Access Token
var personalToken: String?

// OAuth2
var accessToken: String?
var tokenType: String?
Expand All @@ -42,9 +47,14 @@ struct Token: Mappable {
self.basicToken = basicToken
}

init(personalToken: String) {
self.personalToken = personalToken
}

mutating func mapping(map: Map) {
isValid <- map["valid"]
basicToken <- map["basic_token"]
personalToken <- map["personal_token"]
accessToken <- map["access_token"]
tokenType <- map["token_type"]
scope <- map["scope"]
Expand All @@ -54,6 +64,9 @@ struct Token: Mappable {
if let token = basicToken {
return .basic(token: token)
}
if let token = personalToken {
return .personal(token: token)
}
if let token = accessToken {
return .oAuth(token: token)
}
Expand Down
102 changes: 82 additions & 20 deletions SwiftHub/Modules/Login/LoginViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import RxCocoa
import SafariServices

enum LoginSegments: Int {
case oAuth, basic
case oAuth, personal, basic

var title: String {
switch self {
case .oAuth: return R.string.localizable.loginOAuthSegmentTitle.key.localized()
case .personal: return R.string.localizable.loginPersonalSegmentTitle.key.localized()
case .basic: return R.string.localizable.loginBasicSegmentTitle.key.localized()
}
}
Expand All @@ -25,24 +26,20 @@ enum LoginSegments: Int {
class LoginViewController: ViewController {

lazy var segmentedControl: SegmentedControl = {
let items = [LoginSegments.oAuth.title, LoginSegments.basic.title]
let view = SegmentedControl(sectionTitles: items)
let view = SegmentedControl(sectionTitles: [])
view.selectedSegmentIndex = 0
view.snp.makeConstraints({ (make) in
make.width.equalTo(250)
make.width.equalTo(300)
})
return view
}()

lazy var basicLoginStackView: StackView = {
let subviews: [UIView] = [self.basicLogoImageView, self.loginTextField, self.passwordTextField, self.basicLoginButton]
let view = StackView(arrangedSubviews: subviews)
return view
}()
// MARK: - Basic authentication

lazy var oAuthLoginStackView: StackView = {
let subviews: [UIView] = [self.oAuthLogoImageView, self.titleLabel, self.detailLabel, self.oAuthLoginButton]
lazy var basicLoginStackView: StackView = {
let subviews: [UIView] = [basicLogoImageView, loginTextField, passwordTextField, basicLoginButton]
let view = StackView(arrangedSubviews: subviews)
view.spacing = inset * 2
return view
}()

Expand Down Expand Up @@ -73,6 +70,15 @@ class LoginViewController: ViewController {
return view
}()

// MARK: - OAuth authentication

lazy var oAuthLoginStackView: StackView = {
let subviews: [UIView] = [oAuthLogoImageView, titleLabel, detailLabel, oAuthLoginButton]
let view = StackView(arrangedSubviews: subviews)
view.spacing = inset * 2
return view
}()

lazy var oAuthLogoImageView: ImageView = {
let view = ImageView(image: R.image.image_no_result()?.template)
return view
Expand All @@ -81,6 +87,7 @@ class LoginViewController: ViewController {
lazy var titleLabel: Label = {
let view = Label()
view.font = view.font.withSize(22)
view.numberOfLines = 0
view.textAlignment = .center
return view
}()
Expand All @@ -100,6 +107,51 @@ class LoginViewController: ViewController {
return view
}()

// MARK: - Personal Access Token authentication

lazy var personalLoginStackView: StackView = {
let subviews: [UIView] = [personalLogoImageView, personalTitleLabel, personalDetailLabel, personalTokenTextField, personalLoginButton]
let view = StackView(arrangedSubviews: subviews)
view.spacing = inset * 2
return view
}()

lazy var personalLogoImageView: ImageView = {
let view = ImageView(image: R.image.image_no_result()?.template)
return view
}()

lazy var personalTitleLabel: Label = {
let view = Label()
view.font = view.font.withSize(22)
view.numberOfLines = 0
view.textAlignment = .center
return view
}()

lazy var personalDetailLabel: Label = {
let view = Label()
view.font = view.font.withSize(17)
view.numberOfLines = 0
view.textAlignment = .center
return view
}()

lazy var personalTokenTextField: TextField = {
let view = TextField()
view.textAlignment = .center
view.keyboardType = .emailAddress
view.autocapitalizationType = .none
return view
}()

lazy var personalLoginButton: Button = {
let view = Button()
view.imageForNormal = R.image.icon_button_github()
view.centerTextAndImage(spacing: inset)
return view
}()

private lazy var scrollView: ScrollView = {
let view = ScrollView()
self.contentView.addSubview(view)
Expand All @@ -118,34 +170,40 @@ class LoginViewController: ViewController {
override func makeUI() {
super.makeUI()

navigationItem.titleView = segmentedControl

languageChanged.subscribe(onNext: { [weak self] () in
self?.segmentedControl.sectionTitles = [LoginSegments.oAuth.title, LoginSegments.personal.title, LoginSegments.basic.title]
// MARK: Basic
self?.loginTextField.placeholder = R.string.localizable.loginLoginTextFieldPlaceholder.key.localized()
self?.passwordTextField.placeholder = R.string.localizable.loginPasswordTextFieldPlaceholder.key.localized()
self?.basicLoginButton.titleForNormal = R.string.localizable.loginBasicLoginButtonTitle.key.localized()
// MARK: Personal
self?.personalTitleLabel.text = R.string.localizable.loginPersonalTitleLabelText.key.localized()
self?.personalDetailLabel.text = R.string.localizable.loginPersonalDetailLabelText.key.localizedFormat(Configs.App.githubScope)
self?.personalTokenTextField.placeholder = R.string.localizable.loginPersonalTokenTextFieldPlaceholder.key.localized()
self?.personalLoginButton.titleForNormal = R.string.localizable.loginPersonalLoginButtonTitle.key.localized()
// MARK: OAuth
self?.titleLabel.text = R.string.localizable.loginTitleLabelText.key.localized()
self?.detailLabel.text = R.string.localizable.loginDetailLabelText.key.localized()
self?.oAuthLoginButton.titleForNormal = R.string.localizable.loginOAuthloginButtonTitle.key.localized()
self?.segmentedControl.sectionTitles = [LoginSegments.oAuth.title,
LoginSegments.basic.title]
self?.navigationItem.titleView = self?.segmentedControl
}).disposed(by: rx.disposeBag)

stackView.removeFromSuperview()
scrollView.addSubview(stackView)
stackView.snp.makeConstraints({ (make) in
make.top.bottom.equalToSuperview().inset(self.inset*2)
make.edges.equalToSuperview().inset(self.inset*2)
make.centerX.equalToSuperview()
make.width.equalTo(300)
})

themeService.rx
.bind({ $0.text }, to: titleLabel.rx.textColor)
.bind({ $0.textGray }, to: detailLabel.rx.textColor)
.bind({ $0.text }, to: basicLogoImageView.rx.tintColor)
.bind({ $0.text }, to: oAuthLogoImageView.rx.tintColor)
.bind({ $0.text }, to: [titleLabel.rx.textColor, personalTitleLabel.rx.textColor])
.bind({ $0.textGray }, to: [detailLabel.rx.textColor, personalDetailLabel.rx.textColor])
.bind({ $0.text }, to: [basicLogoImageView.rx.tintColor, personalLogoImageView.rx.tintColor, oAuthLogoImageView.rx.tintColor])
.disposed(by: rx.disposeBag)

stackView.addArrangedSubview(basicLoginStackView)
stackView.addArrangedSubview(personalLoginStackView)
stackView.addArrangedSubview(oAuthLoginStackView)
bannerView.isHidden = true
}
Expand All @@ -157,6 +215,7 @@ class LoginViewController: ViewController {
let segmentSelected = Observable.of(segmentedControl.segmentSelection.map { LoginSegments(rawValue: $0)! }).merge()
let input = LoginViewModel.Input(segmentSelection: segmentSelected.asDriverOnErrorJustComplete(),
basicLoginTrigger: basicLoginButton.rx.tap.asDriver(),
personalLoginTrigger: personalLoginButton.rx.tap.asDriver(),
oAuthLoginTrigger: oAuthLoginButton.rx.tap.asDriver())
let output = viewModel.transform(input: input)

Expand All @@ -168,11 +227,14 @@ class LoginViewController: ViewController {
}).disposed(by: rx.disposeBag)

output.basicLoginButtonEnabled.drive(basicLoginButton.rx.isEnabled).disposed(by: rx.disposeBag)
output.personalLoginButtonEnabled.drive(personalLoginButton.rx.isEnabled).disposed(by: rx.disposeBag)

_ = loginTextField.rx.textInput <-> viewModel.login
_ = passwordTextField.rx.textInput <-> viewModel.password
_ = personalTokenTextField.rx.textInput <-> viewModel.personalToken

output.hidesBasicLoginView.drive(basicLoginStackView.rx.isHidden).disposed(by: rx.disposeBag)
output.hidesPersonalLoginView.drive(personalLoginStackView.rx.isHidden).disposed(by: rx.disposeBag)
output.hidesOAuthLoginView.drive(oAuthLoginStackView.rx.isHidden).disposed(by: rx.disposeBag)

error.subscribe(onNext: { [weak self] (error) in
Expand Down
66 changes: 40 additions & 26 deletions SwiftHub/Modules/Login/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,34 @@
import Foundation
import RxCocoa
import RxSwift
import RxSwiftExt
import SafariServices

private let loginURL = URL(string: "http://github.com/login/oauth/authorize?client_id=\(Keys.github.appId)&scope=user+repo+notifications+read:org")!
private let loginURL = URL(string: "http://github.com/login/oauth/authorize?client_id=\(Keys.github.appId)&scope=\(Configs.App.githubScope)")!
private let callbackURLScheme = "swifthub"

class LoginViewModel: ViewModel, ViewModelType {

struct Input {
let segmentSelection: Driver<LoginSegments>
let basicLoginTrigger: Driver<Void>
let personalLoginTrigger: Driver<Void>
let oAuthLoginTrigger: Driver<Void>
}

struct Output {
let basicLoginTriggered: Driver<Void>
let oAuthLoginTriggered: Driver<Void>
let basicLoginButtonEnabled: Driver<Bool>
let personalLoginButtonEnabled: Driver<Bool>
let hidesBasicLoginView: Driver<Bool>
let hidesPersonalLoginView: Driver<Bool>
let hidesOAuthLoginView: Driver<Bool>
}

let login = BehaviorRelay(value: "")
let password = BehaviorRelay(value: "")

let personalToken = BehaviorRelay(value: "")

let code = PublishSubject<String>()

var tokenSaved = PublishSubject<Void>()
Expand All @@ -49,10 +53,8 @@ class LoginViewModel: ViewModel, ViewModelType {
}

func transform(input: Input) -> Output {
let basicLoginTriggered = input.basicLoginTrigger
let oAuthLoginTriggered = input.oAuthLoginTrigger

basicLoginTriggered.drive(onNext: { [weak self] () in
input.basicLoginTrigger.drive(onNext: { [weak self] () in
if let login = self?.login.value,
let password = self?.password.value,
let authHash = "\(login):\(password)".base64Encoded {
Expand All @@ -61,7 +63,14 @@ class LoginViewModel: ViewModel, ViewModelType {
}
}).disposed(by: rx.disposeBag)

oAuthLoginTriggered.drive(onNext: { [weak self] () in
input.personalLoginTrigger.drive(onNext: { [weak self] () in
if let personalToken = self?.personalToken.value {
AuthManager.setToken(token: Token(personalToken: personalToken))
self?.tokenSaved.onNext(())
}
}).disposed(by: rx.disposeBag)

input.oAuthLoginTrigger.drive(onNext: { [weak self] () in
self?.authSession = SFAuthenticationSession(url: loginURL, callbackURLScheme: callbackURLScheme, completionHandler: { (callbackUrl, error) in
if let error = error {
logError(error.localizedDescription)
Expand Down Expand Up @@ -91,38 +100,43 @@ class LoginViewModel: ViewModel, ViewModelType {
}
}).disposed(by: rx.disposeBag)

tokenSaved.flatMapLatest { () -> Observable<RxSwift.Event<User>> in
let request = tokenSaved.flatMapLatest { () -> Observable<RxSwift.Event<User>> in
return self.provider.profile()
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { (event) in
switch event {
case .next(let user):
user.save()
AuthManager.tokenValidated()
if let login = user.login, let type = AuthManager.shared.token?.type().description {
analytics.log(.login(login: login, type: type))
}
Application.shared.presentInitialScreen(in: Application.shared.window)
case .error(let error):
logError(error.localizedDescription)
AuthManager.removeToken()
default: break
}
}).disposed(by: rx.disposeBag)
}.share()

request.elements().subscribe(onNext: { (user) in
user.save()
AuthManager.tokenValidated()
if let login = user.login, let type = AuthManager.shared.token?.type().description {
analytics.log(.login(login: login, type: type))
}
Application.shared.presentInitialScreen(in: Application.shared.window)
}).disposed(by: rx.disposeBag)

request.errors().subscribe(onNext: { (error) in
logError(error.localizedDescription)
AuthManager.removeToken()
}).disposed(by: rx.disposeBag)

let basicLoginButtonEnabled = BehaviorRelay.combineLatest(login, password, self.loading.asObservable()) {
return $0.isNotEmpty && $1.isNotEmpty && !$2
}.asDriver(onErrorJustReturn: false)

let personalLoginButtonEnabled = BehaviorRelay.combineLatest(personalToken, self.loading.asObservable()) {
return $0.isNotEmpty && !$1
}.asDriver(onErrorJustReturn: false)

let hidesBasicLoginView = input.segmentSelection.map { $0 != LoginSegments.basic }
let hidesPersonalLoginView = input.segmentSelection.map { $0 != LoginSegments.personal }
let hidesOAuthLoginView = input.segmentSelection.map { $0 != LoginSegments.oAuth }

return Output(basicLoginTriggered: basicLoginTriggered,
oAuthLoginTriggered: oAuthLoginTriggered,
basicLoginButtonEnabled: basicLoginButtonEnabled,
return Output(basicLoginButtonEnabled: basicLoginButtonEnabled,
personalLoginButtonEnabled: personalLoginButtonEnabled,
hidesBasicLoginView: hidesBasicLoginView,
hidesPersonalLoginView: hidesPersonalLoginView,
hidesOAuthLoginView: hidesOAuthLoginView)
}
}
2 changes: 2 additions & 0 deletions SwiftHub/Networking/Rest/GitHubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ extension GithubAPI: TargetType, ProductAPIType {
switch token.type() {
case .basic(let token):
return ["Authorization": "Basic \(token)"]
case .personal(let token):
return ["Authorization": "token \(token)"]
case .oAuth(let token):
return ["Authorization": "token \(token)"]
case .unauthorized: break
Expand Down
Loading

0 comments on commit 34d3627

Please sign in to comment.