feat: authorize iOS Hermes accounts and secure session isolation
This commit is contained in:
1 parent
9bdaea2408
commit
bebaf59466
23 files changed
+911
-185
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
|
||||
struct AuthUser: Codable, Equatable {
|
||||
let id: String
|
||||
let username: String
|
||||
let displayName: String?
|
||||
let role: String
|
||||
let status: String
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, username, role, status
|
||||
case displayName = "display_name"
|
||||
}
|
||||
}
|
||||
struct AuthLoginResponse: Decodable {
|
||||
let token: String
|
||||
let user: AuthUser
|
||||
let expiresAt: Double
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case token, user
|
||||
case expiresAt = "expires_at"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
token = try c.decode(String.self, forKey: .token)
|
||||
user = try c.decode(AuthUser.self, forKey: .user)
|
||||
if let seconds = try? c.decode(Double.self, forKey: .expiresAt) { expiresAt = seconds }
|
||||
else {
|
||||
let raw = try c.decode(String.self, forKey: .expiresAt)
|
||||
guard let date = ISO8601DateFormatter().date(from: raw) else { throw AuthError.invalidResponse }
|
||||
expiresAt = date.timeIntervalSince1970
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
enum AuthError: LocalizedError {
|
||||
case status(Int)
|
||||
case invalidResponse
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .status(401): return "Your sign-in expired or the username and password were not accepted. Sign in again."
|
||||
case .status(403): return "This account is not authorized. Contact your server administrator."
|
||||
case .status: return "The server could not complete sign-in. Try again later."
|
||||
case .invalidResponse: return "The server returned an unexpected authentication response."
|
||||
}
|
||||
}
|
||||
}
|
||||
final class AuthClient {
|
||||
let config: ServerConfig
|
||||
private let session: URLSession
|
||||
init(config: ServerConfig, session: URLSession? = nil) {
|
||||
self.config = config
|
||||
self.session = session ?? TLSPinning.session()
|
||||
}
|
||||
private func request(_ path: String, method: String, token: String? = nil, body: Data? = nil) async throws -> Data {
|
||||
var request = URLRequest(url: config.baseURL.appendingPathComponent(path))
|
||||
request.httpMethod = method
|
||||
request.timeoutInterval = 10
|
||||
request.httpBody = body
|
||||
if body != nil { request.setValue("application/json", forHTTPHeaderField: "Content-Type") }
|
||||
if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else { throw AuthError.invalidResponse }
|
||||
guard (200..<300).contains(http.statusCode) else { throw AuthError.status(http.statusCode) }
|
||||
return data
|
||||
}
|
||||
func login(username: String, password: String) async throws -> AuthLoginResponse {
|
||||
let body = try JSONSerialization.data(withJSONObject: ["username": username, "password": password])
|
||||
return try JSONDecoder().decode(AuthLoginResponse.self, from: await request("api/auth/login", method: "POST", body: body))
|
||||
}
|
||||
func me(token: String) async throws -> AuthUser {
|
||||
struct Response: Decodable { let user: AuthUser }
|
||||
return try JSONDecoder().decode(Response.self, from: await request("api/auth/me", method: "GET", token: token)).user
|
||||
}
|
||||
func logout(token: String) async throws {
|
||||
_ = try await request("api/auth/logout", method: "POST", token: token)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user