Initial commit
This commit is contained in:
150
apple-client/Sources/App/BackendClient.swift
Normal file
150
apple-client/Sources/App/BackendClient.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
import Foundation
|
||||
|
||||
struct BackendClient {
|
||||
let baseURL: URL
|
||||
|
||||
func register(username: String, password: String, displayName: String) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/auth/register",
|
||||
body: RegisterRequest(
|
||||
username: username,
|
||||
password: password,
|
||||
displayName: displayName.isEmpty ? nil : displayName
|
||||
),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/auth/login",
|
||||
body: LoginRequest(username: username, password: password),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
func restoreSession(token: String) async throws -> SessionResponse {
|
||||
try await get(path: "/api/auth/session", token: token)
|
||||
}
|
||||
|
||||
func logout(token: String) async throws {
|
||||
let _: EmptyResponse = try await post(path: "/api/auth/logout", body: EmptyBody(), token: token)
|
||||
}
|
||||
|
||||
func listAccessKeys(token: String) async throws -> [AccessKeySummary] {
|
||||
let response: AccessKeyListResponse = try await get(path: "/api/webauthn/credentials", token: token)
|
||||
return response.credentials
|
||||
}
|
||||
|
||||
func startAccessKeyRegistration(label: String?, token: String) async throws -> RegistrationOptionsResponse {
|
||||
try await post(
|
||||
path: "/api/webauthn/register/options",
|
||||
body: RegisterAccessKeyRequest(label: label?.isEmpty == false ? label : nil),
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
func finishAccessKeyRegistration(payload: PasskeyRegistrationPayload, token: String) async throws {
|
||||
let _: EmptyResponse = try await post(
|
||||
path: "/api/webauthn/register/verify",
|
||||
body: VerifyRegistrationRequest(credential: payload),
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
func startAccessKeyAuthentication() async throws -> AuthenticationOptionsResponse {
|
||||
try await post(path: "/api/webauthn/authenticate/options", body: EmptyBody(), token: nil)
|
||||
}
|
||||
|
||||
func finishAccessKeyAuthentication(
|
||||
attemptId: String,
|
||||
payload: PasskeyAuthenticationPayload
|
||||
) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/webauthn/authenticate/verify",
|
||||
body: VerifyAuthenticationRequest(attemptId: attemptId, credential: payload),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func get<Response: Decodable>(path: String, token: String?) async throws -> Response {
|
||||
var request = URLRequest(url: baseURL.appending(path: path))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
if let token {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
return try await perform(request)
|
||||
}
|
||||
|
||||
private func post<Body: Encodable, Response: Decodable>(
|
||||
path: String,
|
||||
body: Body,
|
||||
token: String?
|
||||
) async throws -> Response {
|
||||
var request = URLRequest(url: baseURL.appending(path: path))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
request.httpBody = try JSONEncoder().encode(body)
|
||||
return try await perform(request)
|
||||
}
|
||||
|
||||
private func perform<Response: Decodable>(_ request: URLRequest) async throws -> Response {
|
||||
let (data, rawResponse) = try await URLSession.shared.data(for: request)
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
|
||||
guard let response else {
|
||||
throw APIErrorResponse(message: "The backend did not return a valid HTTP response.")
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
if (200 ..< 300).contains(response.statusCode) {
|
||||
if Response.self == EmptyResponse.self {
|
||||
return EmptyResponse() as! Response
|
||||
}
|
||||
|
||||
return try decoder.decode(Response.self, from: data)
|
||||
}
|
||||
|
||||
if let apiError = try? decoder.decode(APIErrorResponse.self, from: data) {
|
||||
throw apiError
|
||||
}
|
||||
|
||||
throw APIErrorResponse(message: "The backend returned HTTP \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptyBody: Encodable {}
|
||||
private struct EmptyResponse: Decodable {}
|
||||
private struct RegisterRequest: Encodable {
|
||||
let username: String
|
||||
let password: String
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
private struct LoginRequest: Encodable {
|
||||
let username: String
|
||||
let password: String
|
||||
}
|
||||
|
||||
private struct RegisterAccessKeyRequest: Encodable {
|
||||
let label: String?
|
||||
}
|
||||
|
||||
private struct VerifyRegistrationRequest: Encodable {
|
||||
let credential: PasskeyRegistrationPayload
|
||||
}
|
||||
|
||||
private struct VerifyAuthenticationRequest: Encodable {
|
||||
let attemptId: String
|
||||
let credential: PasskeyAuthenticationPayload
|
||||
}
|
||||
Reference in New Issue
Block a user