diff --git a/Sources/KeychainHelper.swift b/Sources/KeychainHelper.swift index 2eb4db4..579e732 100644 --- a/Sources/KeychainHelper.swift +++ b/Sources/KeychainHelper.swift @@ -9,7 +9,7 @@ import Foundation import Security enum KeychainHelper { - static func save(apiKey: String, for hostname: String) { + static func saveApiKey(_ apiKey: String, for hostname: String) { let data = Data(apiKey.utf8) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, diff --git a/Sources/Model/API/ServerAPI.swift b/Sources/Model/API/ServerAPI.swift deleted file mode 100644 index 3e84990..0000000 --- a/Sources/Model/API/ServerAPI.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// ServerAPI.swift -// iKeyMon -// -// Created by tracer on 06.04.25. -// -import Foundation - -final class ServerAPI { - private let hostname: String - private let apiKey: String - - init(hostname: String, apiKey: String) { - self.hostname = hostname - self.apiKey = apiKey - } - - init?(server: Server) { - guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) else { - return nil - } - self.hostname = server.hostname - self.apiKey = apiKey - } - - @discardableResult - func ping() async -> Bool { - guard let url = URL(string: "https://\(hostname)/api/v2/ping") else { - print("❌ [ServerAPI] Invalid ping URL for hostname: \(hostname)") - return false - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") - request.timeoutInterval = 10 - - do { - let (data, response) = try await URLSession.shared.data(for: request) - - // Add debug info for comparison with server info request - if let httpResponse = response as? HTTPURLResponse { - if httpResponse.statusCode != 200 { - print("❌ [ServerAPI] Ping HTTP Status: \(httpResponse.statusCode) for \(hostname)") - if let responseString = String(data: data, encoding: .utf8) { - print("❌ [ServerAPI] Ping error response: \(responseString)") - } - } - } - - if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" { - return true - } else { - print("❌ [ServerAPI] Ping failed - invalid response for \(hostname)") - if let responseString = String(data: data, encoding: .utf8) { - print("❌ [ServerAPI] Ping response: \(responseString)") - } - return false - } - } catch { - print("❌ [ServerAPI] Ping error for \(hostname): \(error)") - return false - } - } - - func fetchServerInfo() async throws -> ServerInfo { - print("🔍 [ServerAPI] Starting fetchServerInfo for hostname: \(hostname)") - - guard let url = URL(string: "https://\(hostname)/api/v2/server") else { - print("❌ [ServerAPI] Invalid URL for hostname: \(hostname)") - throw URLError(.badURL) - } - - print("🌐 [ServerAPI] URL created: \(url.absoluteString)") - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") - // Temporarily remove Content-Type to match ping request exactly - // request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.timeoutInterval = 30 - - print("📤 [ServerAPI] Making request with timeout: \(request.timeoutInterval)s") - print("📤 [ServerAPI] API Key (first 10 chars): \(String(apiKey.prefix(10)))...") - print("📤 [ServerAPI] Request headers: \(request.allHTTPHeaderFields ?? [:])") - - do { - let startTime = Date() - let (data, response) = try await URLSession.shared.data(for: request) - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - - print("📥 [ServerAPI] Request completed in \(String(format: "%.2f", duration))s") - - if let httpResponse = response as? HTTPURLResponse { - print("📥 [ServerAPI] HTTP Status: \(httpResponse.statusCode)") - - // Handle 401 specifically - if httpResponse.statusCode == 401 { - print("❌ [ServerAPI] 401 Unauthorized - API key issue!") - if let responseString = String(data: data, encoding: .utf8) { - print("❌ [ServerAPI] 401 Response body: \(responseString)") - } - throw URLError(.userAuthenticationRequired) - } - - // Handle other non-200 status codes - if httpResponse.statusCode != 200 { - print("❌ [ServerAPI] Non-200 status code: \(httpResponse.statusCode)") - if let responseString = String(data: data, encoding: .utf8) { - print("❌ [ServerAPI] Error response body: \(responseString)") - } - throw URLError(.badServerResponse) - } - } - - print("📥 [ServerAPI] Data size: \(data.count) bytes") - - print("🔄 [ServerAPI] Decoding response payload...") - - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - let decodedResponse = try decoder.decode(ServerResponseEnvelope.self, from: data) - let decoded = decodedResponse.toDomain() - print("✅ [ServerAPI] JSON decoding successful") - - return decoded - } catch let urlError as URLError { - print("❌ [ServerAPI] URLError: \(urlError.localizedDescription)") - print("❌ [ServerAPI] URLError code: \(urlError.code.rawValue)") - throw urlError - } catch let decodingError as DecodingError { - print("❌ [ServerAPI] Decoding error: \(decodingError)") - throw decodingError - } catch { - print("❌ [ServerAPI] Unexpected error: \(error)") - throw error - } - } -} - -// MARK: - Response Helpers - -private extension ServerAPI { - struct ServerResponseEnvelope: Decodable { - let data: ServerData - let meta: Meta - - struct Meta: Decodable { - let apiVersion: String - } - } - - struct ServerData: Decodable { - struct Port: Decodable { - let service: String - let status: String - let port: Int - let proto: String - } - - struct Load: Decodable { - let minute1: Double - let minute5: Double - let minute15: Double - let percent: Double - let cpuCount: Int - let level: String - } - - struct Memory: Decodable { - let free: Int - let used: Int - let total: Int - let percent: Double - } - - struct Disk: Decodable { - let free: Int - let used: Int - let total: Int - let percent: Double - } - - struct PHPInterpreter: Decodable { - let version: String - let path: String? - let configFile: String? - let extensions: [String] - let memoryLimit: String? - let maxExecutionTime: String? - } - - let hostname: String - let ipAddresses: [String] - let cpuCores: Int - let serverTime: String - let uptime: String - let processCount: Int - let apacheVersion: String - let phpVersion: String - let mysqlVersion: String? - let mariadbVersion: String? - let ports: [Port]? - let load: Load - let memory: Memory - let swap: Memory - let diskSpace: Disk - let panelVersion: String - let panelBuild: String - let additionalPhpInterpreters: [PHPInterpreter]? - } - -} - -private extension ServerAPI.ServerResponseEnvelope { - func toDomain() -> ServerInfo { - ServerInfo( - hostname: data.hostname, - ipAddresses: data.ipAddresses, - cpuCores: data.cpuCores, - serverTime: data.serverTime, - uptime: data.uptime, - processCount: data.processCount, - emailsInQueue: nil, - apacheVersion: data.apacheVersion, - phpVersion: data.phpVersion, - mysqlVersion: data.mysqlVersion, - mariadbVersion: data.mariadbVersion, - operatingSystem: nil, - ports: data.ports?.map { ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto) }, - load: ServerInfo.Load( - minute1: data.load.minute1, - minute5: data.load.minute5, - minute15: data.load.minute15, - percent: data.load.percent, - cpuCount: data.load.cpuCount, - level: data.load.level - ), - memory: ServerInfo.Memory( - free: data.memory.free, - used: data.memory.used, - total: data.memory.total, - percent: data.memory.percent - ), - swap: ServerInfo.Memory( - free: data.swap.free, - used: data.swap.used, - total: data.swap.total, - percent: data.swap.percent - ), - diskSpace: ServerInfo.DiskSpace( - free: data.diskSpace.free, - used: data.diskSpace.used, - total: data.diskSpace.total, - percent: data.diskSpace.percent - ), - panelVersion: data.panelVersion, - panelBuild: data.panelBuild, - apiVersion: meta.apiVersion, - additionalPHPInterpreters: data.additionalPhpInterpreters?.map { - ServerInfo.PHPInterpreter( - version: $0.version, - path: $0.path, - configFile: $0.configFile, - extensions: $0.extensions, - memoryLimit: $0.memoryLimit, - maxExecutionTime: $0.maxExecutionTime - ) - } - ) - } -} diff --git a/Sources/Model/API/ServerInfo.swift b/Sources/Model/API/ServerInfo.swift index cd072eb..67cd172 100644 --- a/Sources/Model/API/ServerInfo.swift +++ b/Sources/Model/API/ServerInfo.swift @@ -145,8 +145,8 @@ struct ServerInfo: Codable, Hashable, Equatable { var phpVersion: String var mysqlVersion: String? var mariadbVersion: String? - var operatingSystem: OperatingSystem? var emailsInQueue: Int? + var operatingSystem: OperatingSystem? var ports: [ServicePort]? var load: Load var memory: Memory diff --git a/Sources/Model/API/ServerTypes.swift b/Sources/Model/API/ServerTypes.swift deleted file mode 100644 index 3544041..0000000 --- a/Sources/Model/API/ServerTypes.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// ServerTypes_Fixed.swift -// iKeyMon -// -// Fixed server types matching your existing code structure -// - -import Foundation - -// MARK: - Server API Response Types -struct ServerAPIResponse: Codable { - let data: T - let status: String - let timestamp: Date - let version: String - - enum CodingKeys: String, CodingKey { - case data - case status - case timestamp - case version - } -} - -// MARK: - Server API Version -struct ServerAPIVersion: Codable { - let major: Int - let minor: Int - let patch: Int - let string: String - - init(major: Int, minor: Int, patch: Int = 0) { - self.major = major - self.minor = minor - self.patch = patch - self.string = "\(major).\(minor).\(patch)" - } - - init(from versionString: String) throws { - let components = versionString.split(separator: ".").compactMap { Int($0) } - guard components.count >= 2 else { - throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: [], debugDescription: "Invalid version string format") - ) - } - - self.major = components[0] - self.minor = components[1] - self.patch = components.count > 2 ? components[2] : 0 - self.string = versionString - } -} - -// MARK: - Server API Response Factory -class ServerAPIResponseFactory { - static func createResponse(data: T, status: String = "success") -> ServerAPIResponse { - return ServerAPIResponse( - data: data, - status: status, - timestamp: Date(), - version: "1.0" - ) - } - - static func createErrorResponse(error: String) -> ServerAPIResponse { - return ServerAPIResponse( - data: error, - status: "error", - timestamp: Date(), - version: "1.0" - ) - } -} diff --git a/Sources/Model/API/Versions/APIv2_12.swift b/Sources/Model/API/Versions/APIv2_12.swift index ad19d79..83dfbad 100644 --- a/Sources/Model/API/Versions/APIv2_12.swift +++ b/Sources/Model/API/Versions/APIv2_12.swift @@ -131,10 +131,10 @@ class APIv2_12: BaseAPIClient, ServerAPIProtocol { typealias UtilizationType = APIv2_12.Utilization private enum Endpoint: String { - case systemInfo = "/api/v2.12/system/info" - case load = "/api/v2.12/metrics/load" - case memory = "/api/v2.12/metrics/memory" - case utilization = "/api/v2.12/metrics/utilization" + case systemInfo = "/api/v2/system/info" + case load = "/api/v2/metrics/load" + case memory = "/api/v2/metrics/memory" + case utilization = "/api/v2/metrics/utilization" func url(baseURL: URL) -> URL { return baseURL.appendingPathComponent(self.rawValue) @@ -342,11 +342,11 @@ private extension APIv2_12 { serverTime: meta.serverTime, uptime: meta.uptime.formatted, processCount: utilization.processCount, - emailsInQueue: utilization.emailsInQueue, apacheVersion: components.apache, phpVersion: components.php, mysqlVersion: components.mysql, mariadbVersion: components.mariadb, + emailsInQueue: utilization.emailsInQueue, operatingSystem: operatingSystem.map { ServerInfo.OperatingSystem( label: $0.label, diff --git a/Sources/Model/API/Versions/APIv2_13.swift b/Sources/Model/API/Versions/APIv2_13.swift index f46c34f..ec3fccd 100644 --- a/Sources/Model/API/Versions/APIv2_13.swift +++ b/Sources/Model/API/Versions/APIv2_13.swift @@ -131,10 +131,10 @@ class APIv2_13: BaseAPIClient, ServerAPIProtocol { typealias UtilizationType = APIv2_13.Utilization private enum Endpoint: String { - case systemInfo = "/api/v2.13/system/info" - case load = "/api/v2.13/metrics/load" - case memory = "/api/v2.13/metrics/memory" - case utilization = "/api/v2.13/metrics/utilization" + case systemInfo = "/api/v2/system/info" + case load = "/api/v2/metrics/load" + case memory = "/api/v2/metrics/memory" + case utilization = "/api/v2/metrics/utilization" func url(baseURL: URL) -> URL { return baseURL.appendingPathComponent(self.rawValue) @@ -342,11 +342,11 @@ private extension APIv2_13 { serverTime: meta.serverTime, uptime: meta.uptime.formatted, processCount: utilization.processCount, - emailsInQueue: utilization.emailsInQueue, apacheVersion: components.apache, phpVersion: components.php, mysqlVersion: components.mysql, mariadbVersion: components.mariadb, + emailsInQueue: utilization.emailsInQueue, operatingSystem: operatingSystem.map { ServerInfo.OperatingSystem( label: $0.label, diff --git a/Sources/Views/ServerFormView.swift b/Sources/Views/ServerFormView.swift index 299ffbc..967e64a 100644 --- a/Sources/Views/ServerFormView.swift +++ b/Sources/Views/ServerFormView.swift @@ -185,7 +185,7 @@ struct ServerFormView: View { print("adding server") let newServer = Server(hostname: trimmedHost) servers.append(newServer) - KeychainHelper.save(apiKey: trimmedKey, for: trimmedHost) + KeychainHelper.saveApiKey(trimmedKey, for: trimmedHost) saveServers() case .edit(let oldServer): if let index = servers.firstIndex(where: { $0.id == oldServer.id }) { @@ -194,7 +194,7 @@ struct ServerFormView: View { if oldHostname != trimmedHost { KeychainHelper.deleteApiKey(for: oldHostname) } - KeychainHelper.save(apiKey: trimmedKey, for: trimmedHost) + KeychainHelper.saveApiKey(trimmedKey, for: trimmedHost) } } } @@ -214,7 +214,7 @@ struct ServerFormView: View { if oldHostname != hostname { KeychainHelper.deleteApiKey(for: oldHostname) } - KeychainHelper.save(apiKey: apiKey, for: hostname) + KeychainHelper.saveApiKey(apiKey, for: hostname) saveServers() } }