initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
102
Proximity/Services/Auth/AuthService.swift
Normal file
102
Proximity/Services/Auth/AuthService.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Orchestrates authentication and owns the authenticated session.
|
||||
///
|
||||
/// **Philosophy:** Auth establishes *account* identity (who you are to the
|
||||
/// app) — it never, by itself, reveals anything to other users. Social
|
||||
/// identity is a separate, opt-in concern handled by `ProfileImportService`
|
||||
/// at reveal time. This separation is what keeps the "anonymous until mutual
|
||||
/// consent" promise intact.
|
||||
@MainActor
|
||||
final class AuthService: ObservableObject {
|
||||
|
||||
@Published private(set) var state: AuthState = .signedOut
|
||||
@Published private(set) var account: Account?
|
||||
|
||||
private let providers: [AuthProviderID: AuthProvider]
|
||||
private let apiClient: APIClient
|
||||
private let keychain: KeychainStore
|
||||
|
||||
init(
|
||||
apiClient: APIClient,
|
||||
keychain: KeychainStore = KeychainStore(),
|
||||
providers: [AuthProviderID: AuthProvider] = [
|
||||
.apple: AppleAuthProvider(),
|
||||
.google: GoogleAuthProvider()
|
||||
]
|
||||
) {
|
||||
self.apiClient = apiClient
|
||||
self.keychain = keychain
|
||||
self.providers = providers
|
||||
}
|
||||
|
||||
/// Restore a previously authenticated session from the keychain.
|
||||
func restoreSession() async {
|
||||
guard let session = keychain.loadSession() else { return }
|
||||
state = .authenticated(session)
|
||||
account = session.account
|
||||
}
|
||||
|
||||
/// Sign in with a given provider.
|
||||
func signIn(with providerID: AuthProviderID) async throws {
|
||||
guard let provider = providers[providerID] else {
|
||||
throw AuthError.providerUnavailable
|
||||
}
|
||||
|
||||
state = .authenticating
|
||||
do {
|
||||
let result = try await provider.signIn()
|
||||
|
||||
// Exchange the provider ID token for a Proximity session token.
|
||||
let session = try await apiClient.exchangeToken(result)
|
||||
|
||||
keychain.save(session)
|
||||
state = .authenticated(session)
|
||||
account = session.account
|
||||
} catch {
|
||||
state = .signedOut
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign out locally and revoke the backend session.
|
||||
func signOut() async {
|
||||
if case let .authenticated(session) = state {
|
||||
await apiClient.revokeSession(session)
|
||||
}
|
||||
keychain.clearSession()
|
||||
state = .signedOut
|
||||
account = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The user's authenticated account (account identity only).
|
||||
struct Account: Codable, Hashable {
|
||||
let id: String
|
||||
let providerID: AuthProviderID
|
||||
let displayName: String
|
||||
let email: String?
|
||||
}
|
||||
|
||||
/// The session state machine.
|
||||
enum AuthState: Equatable {
|
||||
case signedOut
|
||||
case authenticating
|
||||
case authenticated(Session)
|
||||
|
||||
static func == (lhs: AuthState, rhs: AuthState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.signedOut, .signedOut): return true
|
||||
case (.authenticating, .authenticating): return true
|
||||
case (.authenticated(let a), .authenticated(let b)): return a.token == b.token
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A verified Proximity session.
|
||||
struct Session: Codable {
|
||||
let token: String
|
||||
let account: Account
|
||||
}
|
||||
Reference in New Issue
Block a user