Skip to content

2FA support and bug fix #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions V2er.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
5D88D5DC26C2016000302265 /* ExploreReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D88D5DB26C2016000302265 /* ExploreReducer.swift */; };
5D88D5DE26C2017400302265 /* MessageReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D88D5DD26C2017400302265 /* MessageReducer.swift */; };
5D88D5E026C2017E00302265 /* MeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D88D5DF26C2017E00302265 /* MeReducer.swift */; };
5D8D752627656EF900460478 /* TwoStepLoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8D752527656EF900460478 /* TwoStepLoginPage.swift */; };
5D8FAA34272A70F50067766E /* Atributika in Frameworks */ = {isa = PBXBuildFile; productRef = 5D8FAA33272A70F50067766E /* Atributika */; };
5D8FAA36272AD27A0067766E /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8FAA35272AD27A0067766E /* TestView.swift */; };
5D8FAA38272D26200067766E /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8FAA37272D26200067766E /* RootView.swift */; };
Expand Down Expand Up @@ -252,6 +253,7 @@
5D88D5DB26C2016000302265 /* ExploreReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreReducer.swift; sourceTree = "<group>"; };
5D88D5DD26C2017400302265 /* MessageReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReducer.swift; sourceTree = "<group>"; };
5D88D5DF26C2017E00302265 /* MeReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeReducer.swift; sourceTree = "<group>"; };
5D8D752527656EF900460478 /* TwoStepLoginPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepLoginPage.swift; sourceTree = "<group>"; };
5D8FAA35272AD27A0067766E /* TestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = "<group>"; };
5D8FAA37272D26200067766E /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
5D91F8D426F22A6F0089D72E /* TagDetailState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailState.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -360,6 +362,7 @@
isa = PBXGroup;
children = (
5DD4639D26F70CE800A1FBA1 /* LoginPage.swift */,
5D8D752527656EF900460478 /* TwoStepLoginPage.swift */,
);
path = Login;
sourceTree = "<group>";
Expand Down Expand Up @@ -912,6 +915,7 @@
5DD4639E26F70CE800A1FBA1 /* LoginPage.swift in Sources */,
5D1D7B8B26FD7FCE008E0C08 /* DailyInfo.swift in Sources */,
5DA2AD4426C18121007FB1EF /* FeedState.swift in Sources */,
5D8D752627656EF900460478 /* TwoStepLoginPage.swift in Sources */,
5D843E9526A4537C00C47D95 /* SearchPage.swift in Sources */,
5D612FA326C7C61C0009B8F9 /* Endpoint.swift in Sources */,
5D2B2B3C26FF754F00446F93 /* Persist.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions V2er/General/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,24 @@ struct RootHostView: View {
$store.appState.globalState.toast
}

var loginState: Binding<LoginState> {
$store.appState.loginState
}

var body: some View {
MainPage()
.buttonStyle(.plain)
.toast(isPresented: toast.isPresented) {
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
}
.sheet(isPresented: loginState.showLoginView) {
LoginPage()
}
.overlay {
if loginState.raw.showTwoStepDialog {
TwoStepLoginPage()
}
}

}
}
6 changes: 3 additions & 3 deletions V2er/State/DataFlow/Actions/MeActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

struct MeActions {
private static let R: Reducer = .me
struct ShowLoginPageAction: Action {
var target: Reducer = R
}
// struct ShowLoginPageAction: Action {
// var target: Reducer = R
// }
}
12 changes: 12 additions & 0 deletions V2er/State/DataFlow/Model/FeedDetailInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct FeedDetailInfo: BaseModel {
self.headerInfo?.id = id
}

func isValid() -> Bool {
headerInfo != nil && headerInfo!.isValid()
}

struct HeaderInfo: HtmlParsable, FeedItemProtocol {
var id: String = .empty
// div.box img.avatar, .src
Expand Down Expand Up @@ -65,6 +69,10 @@ struct FeedDetailInfo: BaseModel {
// div.box div.header a.op
var appendText: String = .empty

func isValid() -> Bool {
userName.notEmpty && nodeName.notEmpty
}

mutating func update(_ headerInfo: HeaderInfo?) {
guard let headerInfo = headerInfo else { return }
let originalId = self.id
Expand Down Expand Up @@ -138,6 +146,10 @@ struct FeedDetailInfo: BaseModel {
self.html = .default
} else {
self.html = try? root.html()
// append https:// to the images
let img = "src=\"//i.v2ex.co/"
let httpImg = "src=\"https://i.v2ex.co/"
self.html = self.html?.replace(segs: img, with: httpImg)
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion V2er/State/DataFlow/Model/FeedInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ struct FeedInfo: BaseModel {
// @Pick(value = "input.super.special.button", attr = "value")
var unReadNums: Int = 0
// @Pick("form[action=/2fa]")
var twoStepStr: String?
var twoStepStr: String = .empty
// @Pick("div.cell.item")
var items: [Item] = []

func isValid() -> Bool {
if twoStepStr.notEmpty() && twoStepStr.contains("两步验证") {
return false
}
return items.count > 0 || items[0].userName.notEmpty
}

mutating func append(feedInfo: FeedInfo) {
self.unReadNums = feedInfo.unReadNums
self.twoStepStr = feedInfo.twoStepStr
Expand Down
4 changes: 4 additions & 0 deletions V2er/State/DataFlow/Model/TagDetailInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct TagDetailInfo: BaseModel {
// div.box div.cell:has(table)
var topics: [Item] = []

func isValid() -> Bool {
topics.count == 0 || topics[0].userName.notEmpty()
}

struct Item: HtmlItemModel {
// span.item_title a
var id: String = .default
Expand Down
4 changes: 4 additions & 0 deletions V2er/State/DataFlow/Model/UserDetailInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ struct UserDetailInfo: BaseModel {
var topicInfo: TopicInfo = TopicInfo()
var replyInfo: ReplyInfo = ReplyInfo()

func isValid() -> Bool {
userName.notEmpty()
}

struct TopicInfo {
// div.box:has(div.cell_tabs) > div.cell.item
var items: [Item] = []
Expand Down
60 changes: 36 additions & 24 deletions V2er/State/DataFlow/Reducers/LoginReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ func loginReducer(_ state: LoginState, _ action: Action) -> (LoginState, Action?
// Load captcha failed
if case let .invalid(html) = error {
if html.contains("登录受限") {
// Toast.show("登录受限", target: .login)
// Toast.show("登录受限", target: .login)
state.problemHtml = "登录受限\n由于当前 IP 在短时间内的登录尝试次数太多,目前暂时不能继续尝试,你可能会需要等待至多 1 天的时间再继续尝试。"
state.showAlert = true
}
} else {
Toast.show("登录参数加载出错", target: .login)
}
}
case let action as LoginActions.ShowLoginPageAction:
guard !state.showLoginView else { break }
state.showLoginView = true
Toast.show(action.reason, target: .login)
case _ as LoginActions.StartLogin:
guard !state.loading && !state.logining else { break }
state.logining = true
Expand All @@ -53,31 +57,25 @@ func loginReducer(_ state: LoginState, _ action: Action) -> (LoginState, Action?
let loginParam: LoginParams? = APIService.shared.parse(from: html)
guard let loginParam = loginParam else { break }
let problemHtml = loginParam.problem
if loginParam.isValid() {
if problemHtml.isEmpty {
Toast.show("登录失败,用户名和密码无法匹配", target: .login)
} else if problemHtml.notEmpty {
state.problemHtml = problemHtml?.replace(segs: "<ul>","</ul>", "<li>", "</li>" , with: .empty)
state.showAlert = true
} else {
Toast.show("登录中遇到未知问题", target: .login)
}
if problemHtml.isEmpty {
Toast.show("登录失败,用户名和密码无法匹配", target: .login)
} else if problemHtml.notEmpty {
state.problemHtml = problemHtml?.replace(segs: "<ul>","</ul>", "<li>", "</li>" , with: .empty)
state.showAlert = true
} else {
// Check whether enabled two steps login
let twoStepInfo: TwoStepInfo? = APIService.shared.parse(from: html)
if twoStepInfo?.isValid() ?? false {
// Toast.show("暂不支持两步登录", target: .login)
state.showTwoStepDialog = true
}
Toast.show("登录中遇到未知问题", target: .login)
}
} else {
Toast.show(error, target: .login)
}
// -> is LoginParam -> psw error
// -> is TwoStepInfo -> enabled two step log
// dispatch(LoginDone())

}
case let action as LoginActions.ShowTwoStepLogin:
state.showLoginView = false
state.showTwoStepDialog = true
state.twoFAonce = action.once
case _ as LoginActions.TwoStepLogin:
state.showTwoStepDialog = false
// state.showLoginView = false
case let action as LoginActions.TwoStepLoginCancel:
state.showTwoStepDialog = false
case let action as LoginActions.TwoStepLoginDone:
Expand All @@ -87,6 +85,7 @@ func loginReducer(_ state: LoginState, _ action: Action) -> (LoginState, Action?
avatar: twoFALoginInfo!.avatar)
AccountState.saveAccount(account)
state.showTwoStepDialog = false
// todo refresh current page
} else {
Toast.show("2FA登录遇到问题")
}
Expand Down Expand Up @@ -152,11 +151,14 @@ struct LoginActions {

func execute(in store: Store) async {
let state = store.appState.loginState
guard let loginParams = state.loginParams
else { return }
Toast.show("2FA验证中", target: .login)
if state.twoFAonce.isEmpty && state.loginParams == nil { return }
Toast.show("2FA验证中")
var params: Params = [:]
params["once"] = loginParams.once
var once = state.twoFAonce
if once.isEmpty {
once = state.loginParams!.once
}
params["once"] = once
params["code"] = input
var headers: Params = ["Referer": Endpoint.dailyMission.url.absoluteString]
let result: APIResult<TwoStepLoginResultInfo> = await APIService.shared
Expand All @@ -176,4 +178,14 @@ struct LoginActions {
var target: Reducer = R
}

struct ShowLoginPageAction: Action {
var target: Reducer = R
var reason: String = .empty
}

struct ShowTwoStepLogin: Action {
var target: Reducer = R
var once: String
}

}
6 changes: 3 additions & 3 deletions V2er/State/DataFlow/Reducers/MeReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ func meStateReducer(_ state: MeState, _ action: Action) -> (MeState, Action?) {
var followingAction: Action?

switch action {
case let action as MeActions.ShowLoginPageAction:
guard !state.showLoginView else { break }
state.showLoginView = true
// case let action as MeActions.ShowLoginPageAction:
// guard !state.showLoginView else { break }
// state.showLoginView = true
default:
break
}
Expand Down
1 change: 1 addition & 0 deletions V2er/State/DataFlow/State/GlobalState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct GlobalState: FluxState {
static var account: AccountInfo? {
AccountState.getAccount()
}

static var hasSignIn: Bool {
AccountState.hasSignIn()
}
Expand Down
3 changes: 3 additions & 0 deletions V2er/State/DataFlow/State/LoginState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ struct LoginState: FluxState {
var dismiss = false
var toast = Toast()
var problemHtml: String? = .empty

var showLoginView = false
var showAlert: Bool = false
var showTwoStepDialog = false
var twoFAonce: String = .empty
}

struct LoginParams: BaseModel {
Expand Down
2 changes: 1 addition & 1 deletion V2er/State/DataFlow/State/MeState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
import Foundation

struct MeState: FluxState {
var showLoginView = false
// var showLoginView = false
}
50 changes: 40 additions & 10 deletions V2er/State/Networking/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,7 @@ struct APIService {
if result.data == nil {
result.error = APIError.decodingError()
} else if !result.data!.isValid() {
result.error = APIError.invalid(htmlData.string)
// TODO:
/*
Possible general Reasons:
1. need login but no login
2. need login but login session is expired
3. no premission to open the page
4. two step login needed
5. other errors
*/
result.error = handleGeneralError(htmlData.string)
} else {
result.error = nil
}
Expand All @@ -186,6 +177,44 @@ struct APIService {
return result
}

private func handleGeneralError(_ html: String) -> APIError? {
/*
Possible general Reasons:
1. need login but no login
2. need login but login session is expired
3. no premission to open the page
4. two step login needed
5. other errors
*/
// 1. 2FA
let twoStepInfo: TwoStepInfo? = parse(from: html)
if twoStepInfo?.isValid() ?? false {
dispatch(LoginActions.ShowTwoStepLogin(once: twoStepInfo!.once ?? .empty), .default)
return .generalError
}
// 2. Login
let loginParams: LoginParams? = parse(from: html)
if loginParams?.isValid() ?? false {
var reason: String = .empty
if AccountState.hasSignIn() {
// Login session expired
reason = "登录已过期,请重新登录"
} else {
// Need login first
reason = "请先去登录"
}
dispatch(LoginActions.ShowLoginPageAction(reason: reason))
return .generalError
}
// 3. Redirect to home
let feedInfo: FeedInfo? = parse(from: html)
if feedInfo?.isValid() ?? false {
// todo redirect to home
return .generalError
}
return APIError.invalid(html)
}

func parse<T: BaseModel>(from html: String) -> T? {
let parseResult = try? SwiftSoup.parse(html)
let result = T(from: parseResult)
Expand Down Expand Up @@ -242,6 +271,7 @@ enum APIError: Error {
case decodingError(_ rawData: String = "解析出错")
case networkError(_ rawData: String = "网络错误")
case invalid(_ rawData: String)
case generalError
}

struct GeneralError: Error {
Expand Down
Loading