151 lines
4.5 KiB
Swift
151 lines
4.5 KiB
Swift
|
|
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
|
||
|
|
}
|