Skip to content

Commit 213a0c5

Browse files
patch (AccountManager): handle auth failures during runtime
Logs out the current user when the ws closes with an error. Switches to another account if the app starts and the selected user’s token is invalid
1 parent a6639a0 commit 213a0c5

File tree

4 files changed

+55
-21
lines changed

4 files changed

+55
-21
lines changed

Swiftcord/SwiftcordApp.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import DiscordKit
99
import DiscordKitCore
1010
import SwiftUI
11+
import OSLog
1112

1213
// There's probably a better place to put global constants
1314
let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String
@@ -16,8 +17,8 @@ let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String
1617
struct SwiftcordApp: App {
1718
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
1819

19-
static internal let tokenKeychainKey = "authTokens"
20-
static internal let legacyTokenKeychainKey = "authToken"
20+
internal static let tokenKeychainKey = "authTokens"
21+
internal static let legacyTokenKeychainKey = "authToken"
2122

2223
// let persistenceController = PersistenceController.shared
2324
#if !APP_STORE
@@ -30,6 +31,8 @@ struct SwiftcordApp: App {
3031

3132
@AppStorage("theme") private var selectedTheme = "system"
3233

34+
private static let log = Logger(category: "MainApp")
35+
3336
var body: some Scene {
3437
WindowGroup {
3538
if state.attemptLogin {
@@ -59,6 +62,25 @@ struct SwiftcordApp: App {
5962
}
6063
gateway.connect(token: token)
6164
restAPI.setToken(token: token)
65+
_ = gateway.onAuthFailure.addHandler {
66+
Self.log.warning("Auth failed")
67+
guard acctManager.getActiveID() != nil else {
68+
Self.log.error("Current ID not found! Showing login instead.")
69+
state.attemptLogin = true
70+
state.loadingState = .initial
71+
return
72+
}
73+
acctManager.invalidate()
74+
// Switch to other account if possible
75+
if let token = acctManager.getActiveToken() {
76+
Self.log.debug("Attempting connection with other account")
77+
gateway.connect(token: token)
78+
restAPI.setToken(token: token)
79+
} else {
80+
state.attemptLogin = true
81+
state.loadingState = .initial
82+
}
83+
}
6284
}
6385
}
6486
}

Swiftcord/Views/ContentView.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,6 @@ struct ContentView: View {
200200
.onAppear {
201201
if state.loadingState == .messageLoad { loadLastSelectedGuild() }
202202

203-
_ = gateway.onAuthFailure.addHandler {
204-
state.attemptLogin = true
205-
state.loadingState = .initial
206-
log.debug("Attempting login")
207-
}
208203
_ = gateway.onEvent.addHandler { (evt, data) in
209204
switch evt {
210205
case .ready:

Swiftcord/Views/User/AccountSwitcher/AccountMeta.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99
import DiscordKitCommon
1010

11-
struct AccountMeta: Codable, Equatable {
11+
struct AccountMeta: Codable, Equatable, Identifiable {
1212
let id: Snowflake
1313
let discrim: String
1414
let name: String

Swiftcord/Views/User/AccountSwitcher/AccountSwitcher.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class AccountSwitcher: NSObject, ObservableObject {
2727
/// A cache for storing decoded tokens from UserDefaults
2828
private var tokens: [String: String] = [:]
2929

30+
// MARK: - Account metadata methods
3031
func loadAccounts() {
3132
guard let dec = try? JSONDecoder().decode(
3233
[AccountMeta].self,
@@ -39,7 +40,11 @@ public class AccountSwitcher: NSObject, ObservableObject {
3940
func writeAccounts() {
4041
UserDefaults.standard.setValue(try? JSONEncoder().encode(accounts), forKey: AccountSwitcher.META_KEY)
4142
}
43+
func removeAccount(for id: Snowflake) {
44+
accounts.removeAll(identifiedBy: id)
45+
}
4246

47+
// MARK: - Secure token storage methods
4348
func saveToken(for id: Snowflake, token: String) {
4449
tokens[id] = token
4550
// Keychain.save(key: "\(SwiftcordApp.tokenKeychainKey).\(id)", data: token)
@@ -84,34 +89,45 @@ public class AccountSwitcher: NSObject, ObservableObject {
8489
}
8590
func logOut(id: Snowflake) async {
8691
guard let token = getToken(for: id) else { return }
87-
await DiscordREST(token: token).logOut()
88-
Keychain.remove(key: "\(SwiftcordApp.tokenKeychainKey).\(id)")
92+
removeToken(for: id)
8993

9094
DispatchQueue.main.async { [weak self] in
9195
withAnimation {
92-
self?.accounts.removeAll { $0.id == id }
96+
self?.removeAccount(for: id)
9397
self?.writeAccounts()
9498

9599
// Actions to take if the account being logged out is the current one
96100
if UserDefaults.standard.string(forKey: AccountSwitcher.ACTIVE_KEY) == id {
97-
AccountSwitcher.clearAccountSpecificPrefKeys()
98-
if let firstID = self?.accounts.first?.id {
99-
self?.setActiveAccount(id: firstID)
100-
} else {
101-
UserDefaults.standard.removeObject(forKey: AccountSwitcher.ACTIVE_KEY)
102-
}
101+
self?.setActiveAccount(id: self?.accounts.first?.id)
103102
}
104103
}
105104
}
105+
106+
await DiscordREST(token: token).logOut()
107+
}
108+
/// Mark the current user as invalid - i.e. remove it from the token store and acc
109+
///
110+
/// The next account (if present) will automatically become "active" after invalidating the current one.
111+
/// > Note: The user will not be signed out from the Discord API
112+
func invalidate() {
113+
guard let id = getActiveID() else { return }
114+
removeToken(for: id)
115+
removeAccount(for: id)
116+
print("has accounts? \(!accounts.isEmpty)")
117+
setActiveAccount(id: accounts.first?.id)
106118
}
107119

108120
func setPendingToken(token: String) {
109121
pendingToken = token
110122
}
111-
func setActiveAccount(id: Snowflake) {
112-
// ID is always assumed to be correct
113-
UserDefaults.standard.set(id, forKey: AccountSwitcher.ACTIVE_KEY)
123+
func setActiveAccount(id: Snowflake?) {
114124
AccountSwitcher.clearAccountSpecificPrefKeys() // Clear account specific UserDefault keys
125+
if let id = id {
126+
// ID is always assumed to be correct
127+
UserDefaults.standard.set(id, forKey: AccountSwitcher.ACTIVE_KEY)
128+
} else {
129+
UserDefaults.standard.removeObject(forKey: AccountSwitcher.ACTIVE_KEY)
130+
}
115131
}
116132

117133
// Multiple sanity checks ensure account meta is valid, if not, repair is attempted
@@ -121,7 +137,7 @@ public class AccountSwitcher: NSObject, ObservableObject {
121137
AccountSwitcher.log.info("Found token in old key! Logging in with this token...")
122138
return oldToken
123139
}
124-
let storedActiveID = UserDefaults.standard.string(forKey: AccountSwitcher.ACTIVE_KEY)
140+
let storedActiveID = getActiveID()
125141
guard let activeID = storedActiveID != nil && accounts.contains(where: { $0.id == storedActiveID })
126142
? storedActiveID
127143
: accounts.first?.id else { return nil } // Account not signed in
@@ -133,6 +149,7 @@ public class AccountSwitcher: NSObject, ObservableObject {
133149
}
134150
return token
135151
}
152+
func getActiveID() -> String? { UserDefaults.standard.string(forKey: AccountSwitcher.ACTIVE_KEY) }
136153

137154
func onSignedIn(with user: CurrentUser) {
138155
// Migrate from old keychain key to new keys

0 commit comments

Comments
 (0)