75 lines
3.4 KiB
Swift
75 lines
3.4 KiB
Swift
import Foundation
|
|
import PolicyCore
|
|
|
|
struct Credentials: Codable {
|
|
var server: String
|
|
var token: String
|
|
var deviceId: String
|
|
var childId: String
|
|
}
|
|
struct PairResponse: Decodable { let deviceId: String; let childId: String; let token: String }
|
|
struct Acknowledgement: Decodable { let received: Bool }
|
|
struct ConfigurationGrant: Decodable { let authorized: Bool }
|
|
struct MathChallenge: Decodable, Identifiable {
|
|
struct Question: Decodable, Identifiable { let id: String; let prompt: String }
|
|
let id: String
|
|
let expiresAt: Double
|
|
let questions: [Question]
|
|
}
|
|
struct SubmittedAnswer: Encodable { let questionId: String; let answer: Int }
|
|
struct Submission: Encodable { let answers: [SubmittedAnswer] }
|
|
struct ChallengeResult: Decodable {
|
|
let passed: Bool
|
|
let correctCount: Int
|
|
let requiredCount: Int
|
|
let awardedMinutes: Int
|
|
}
|
|
private struct APIError: Decodable { let detail: String }
|
|
|
|
private final class NoRedirects: NSObject, URLSessionTaskDelegate {
|
|
func urlSession(_ session: URLSession, task: URLSessionTask,
|
|
willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest,
|
|
completionHandler: @escaping (URLRequest?) -> Void) {
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
|
|
struct APIClient {
|
|
let origin: URL
|
|
let token: String?
|
|
|
|
init(server: String, token: String? = nil) throws {
|
|
guard let parts = URLComponents(string: server.trimmingCharacters(in: .whitespacesAndNewlines)),
|
|
let host = parts.host, parts.user == nil, parts.password == nil,
|
|
parts.query == nil, parts.fragment == nil, ["", "/"].contains(parts.path),
|
|
let url = parts.url else { throw LocalError.message("Enter the server origin, without a path") }
|
|
var allowed = parts.scheme == "https"
|
|
#if DEBUG
|
|
allowed = allowed || (parts.scheme == "http" && ["localhost", "127.0.0.1", "::1"].contains(host))
|
|
#endif
|
|
guard allowed else { throw LocalError.message("Use HTTPS with a trusted certificate; plain LAN HTTP is not supported") }
|
|
origin = url
|
|
self.token = token
|
|
}
|
|
|
|
func request<T: Decodable>(_ path: String, method: String = "GET", body: Data? = nil) async throws -> T {
|
|
guard let url = URL(string: path, relativeTo: origin)?.absoluteURL,
|
|
url.host == origin.host else { throw LocalError.message("Invalid API path") }
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method; request.httpBody = body; request.timeoutInterval = 20
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.httpShouldSetCookies = false; config.urlCache = nil
|
|
let session = URLSession(configuration: config, delegate: NoRedirects(), delegateQueue: nil)
|
|
defer { session.finishTasksAndInvalidate() }
|
|
let (data, response) = try await session.data(for: request)
|
|
guard let http = response as? HTTPURLResponse else { throw LocalError.message("Invalid server response") }
|
|
guard (200...299).contains(http.statusCode) else {
|
|
let detail = (try? JSONDecoder().decode(APIError.self, from: data).detail) ?? "HTTP \(http.statusCode)"
|
|
throw LocalError.message(detail)
|
|
}
|
|
return try WireCoding.decoder().decode(T.self, from: data)
|
|
}
|
|
}
|