Files

151 lines
4.5 KiB
Swift
Raw Permalink Normal View History

2026-03-09 19:35:08 +01:00
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
}