Archived
feat: authorize iOS Hermes accounts and secure session isolation
This commit is contained in:
1 parent
9bdaea2408
commit
bebaf59466
23 files changed
+911
-185
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
final class AuthStore: ObservableObject {
|
||||
@Published private(set) var user: AuthUser?
|
||||
@Published private(set) var token: String?
|
||||
@Published private(set) var isBusy = false
|
||||
@Published private(set) var error: String?
|
||||
var disconnectAccount: (() async -> Void)?
|
||||
let config: ServerConfig
|
||||
private let client: AuthClient
|
||||
private let tokens: TokenStore
|
||||
private var generation = 0
|
||||
private var operation: Task<AuthUser, Error>?
|
||||
private var loginOperation: Task<AuthLoginResponse, Error>?
|
||||
let fixture: Bool
|
||||
|
||||
init(config: ServerConfig, client: AuthClient? = nil, tokens: TokenStore = KeychainTokenStore(), fixture: Bool = false) {
|
||||
self.config = config
|
||||
self.client = client ?? AuthClient(config: config)
|
||||
self.tokens = tokens
|
||||
self.fixture = fixture
|
||||
if fixture {
|
||||
user = AuthUser(id: "offline-fixture", username: "fixture", displayName: "Offline fixture", role: "user", status: "ready")
|
||||
token = "offline-fixture-not-a-server-token"
|
||||
}
|
||||
}
|
||||
func restore() async {
|
||||
guard !fixture, !isBusy, user == nil else { return }
|
||||
generation += 1
|
||||
let current = generation
|
||||
isBusy = true
|
||||
do {
|
||||
guard let saved = try tokens.token(origin: config.baseURL.absoluteString) else { isBusy = false; return }
|
||||
let task = Task { try await client.me(token: saved) }
|
||||
operation = task
|
||||
let verified = try await task.value
|
||||
guard current == generation, !Task.isCancelled else { return }
|
||||
guard ["ready", "provisioning"].contains(verified.status) else { throw AuthError.status(403) }
|
||||
user = verified
|
||||
token = saved
|
||||
} catch {
|
||||
guard current == generation else { return }
|
||||
if let authError = error as? AuthError,
|
||||
case .status(let status) = authError, status == 401 || status == 403 {
|
||||
try? tokens.save(nil, origin: config.baseURL.absoluteString)
|
||||
self.error = "Please sign in to continue."
|
||||
} else {
|
||||
// An outage is not revocation. Preserve the origin-scoped bearer
|
||||
// for a subsequent launch/retry, but expose no unverified user UI.
|
||||
self.error = "Could not verify your saved sign-in. Check the connection or sign in again."
|
||||
}
|
||||
}
|
||||
guard current == generation else { return }
|
||||
isBusy = false
|
||||
operation = nil
|
||||
}
|
||||
func login(username: String, password: String) async {
|
||||
guard !isBusy else { return }
|
||||
if fixture {
|
||||
user = AuthUser(id: "offline-" + username, username: username, displayName: username, role: "user", status: "ready")
|
||||
token = "offline-fixture-not-a-server-token"
|
||||
return
|
||||
}
|
||||
generation += 1
|
||||
let current = generation
|
||||
isBusy = true
|
||||
error = nil
|
||||
do {
|
||||
let task = Task { try await client.login(username: username, password: password) }
|
||||
loginOperation = task
|
||||
let response = try await task.value
|
||||
guard current == generation, !Task.isCancelled else { return }
|
||||
guard !response.token.isEmpty, ["ready", "provisioning"].contains(response.user.status), response.expiresAt > Date().timeIntervalSince1970 else { throw AuthError.status(403) }
|
||||
try tokens.save(response.token, origin: config.baseURL.absoluteString)
|
||||
token = response.token
|
||||
user = response.user
|
||||
} catch {
|
||||
guard current == generation else { return }
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
guard current == generation else { return }
|
||||
isBusy = false
|
||||
loginOperation = nil
|
||||
}
|
||||
func logout(disconnect: () async -> Void) async {
|
||||
generation += 1
|
||||
loginOperation?.cancel()
|
||||
loginOperation = nil
|
||||
operation?.cancel()
|
||||
operation = nil
|
||||
let previous = token
|
||||
token = nil // immediately disables bootstrap; account UI stays until transport closes
|
||||
isBusy = true
|
||||
await disconnect()
|
||||
try? tokens.save(nil, origin: config.baseURL.absoluteString)
|
||||
user = nil
|
||||
disconnectAccount = nil
|
||||
error = nil
|
||||
if let previous, !fixture { try? await client.logout(token: previous) }
|
||||
isBusy = false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user