Files
2026-10-04 07:23:49 -04:00

94 lines
3.2 KiB
Swift

import Foundation
struct TokenResponse: Decodable, Equatable {
let token: String
let room: String
let user: User
let conversationID: String?
enum CodingKeys: String, CodingKey {
case token, room, user
case conversationID = "conversation_id"
}
struct User: Decodable, Equatable {
let id: String
let displayName: String?
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
}
}
}
enum TokenError: LocalizedError {
case invalidServerURL(String)
case httpStatus(Int, String)
case decodeFailed
case network(Error)
var errorDescription: String? {
switch self {
case .invalidServerURL(let raw):
return "Invalid server URL: \(raw)"
case .httpStatus(let code, let detail):
if code == 503 {
return "Server is starting up or the user file is missing (HTTP 503). Check the deployment on the server. \(detail)"
}
return "Token request failed with HTTP \(code). \(detail)"
case .decodeFailed:
return "Unexpected response from the token endpoint."
case .network(let error):
return "Could not reach the server: \(error.localizedDescription)"
}
}
}
final class TokenClient {
let config: ServerConfig
private let session: URLSession
var onUnauthorized: (() async -> Void)?
let bearerToken: String
var conversationID: String?
init(config: ServerConfig, bearerToken: String = "", session: URLSession? = nil) {
self.config = config
self.session = session ?? TLSPinning.session()
self.bearerToken = bearerToken
}
func cancel() { session.getAllTasks { $0.forEach { $0.cancel() } } }
func fetchToken() async throws -> TokenResponse {
#if DEBUG
if ProcessInfo.processInfo.arguments.contains("--ui-testing"),
!ProcessInfo.processInfo.arguments.contains("--live-testing") {
throw URLError(.cannotConnectToHost) // offline fixtures never contact any configured server
}
#endif
guard !bearerToken.isEmpty else { throw AuthError.status(401) }
var request = URLRequest(url: config.baseURL.appendingPathComponent("token"))
request.httpMethod = "POST"
if let conversationID {
request.httpBody = try JSONSerialization.data(withJSONObject: ["conversation_id": conversationID])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 10
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw TokenError.decodeFailed
}
guard (200..<300).contains(http.statusCode) else {
if http.statusCode == 401 || http.statusCode == 403 { await onUnauthorized?() }
let detail = ""
throw TokenError.httpStatus(http.statusCode, detail)
}
do {
return try JSONDecoder().decode(TokenResponse.self, from: data)
} catch {
throw TokenError.decodeFailed
}
}
}