add astra draft
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user