├── ABM-APIClient ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 64.png │ │ ├── 1024.png │ │ ├── 256 1.png │ │ ├── 32 1.png │ │ ├── 512 1.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Info.plist ├── ABM_APIClientApp.swift ├── ABM_APIClient.entitlements ├── CSVDocument.swift ├── ABMViewModel.swift ├── JWTGenerator.swift ├── Models.swift ├── ABMViewModel+Extensions.swift ├── APIService.swift └── ContentView.swift ├── ABM-APIClient.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcuserdata │ └── someshpathak.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── ABM-APIClientTests └── ABM_APIClientTests.swift ├── ABM-APIClientUITests ├── ABM_APIClientUITestsLaunchTests.swift └── ABM_APIClientUITests.swift └── README.md /ABM-APIClient/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/256 1.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/32 1.png -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathaksomesh06/ABM-API-Client/HEAD/ABM-APIClient/Assets.xcassets/AppIcon.appiconset/512 1.png -------------------------------------------------------------------------------- /ABM-APIClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ABM-APIClient/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ABM-APIClient/ABM_APIClientApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABM_APIClientApp.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ABM_APIClientApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ABM-APIClientTests/ABM_APIClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABM_APIClientTests.swift 3 | // ABM-APIClientTests 4 | // 5 | // Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import Testing 9 | @testable import ABM_APIClient 10 | 11 | struct ABM_APIClientTests { 12 | 13 | @Test func example() async throws { 14 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ABM-APIClient/ABM_APIClient.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ABM-APIClient.xcodeproj/xcuserdata/someshpathak.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ABM-APIClient.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ABM-APIClientUITests/ABM_APIClientUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABM_APIClientUITestsLaunchTests.swift 3 | // ABM-APIClientUITests 4 | // 5 | // Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ABM_APIClientUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ABM-APIClientUITests/ABM_APIClientUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABM_APIClientUITests.swift 3 | // ABM-APIClientUITests 4 | // 5 | // Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ABM_APIClientUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ABM-APIClient/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ABM-APIClient/CSVDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSVDocument.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 24/06/2025. 6 | // 7 | 8 | 9 | import SwiftUI 10 | import UniformTypeIdentifiers 11 | 12 | struct CSVDocument: FileDocument { 13 | static var readableContentTypes: [UTType] { [.commaSeparatedText] } 14 | 15 | var csvString: String 16 | 17 | init(devices: [OrgDevice]) { 18 | var csv = "Serial Number,Name,Model,OS,OS Version,Status,ID\n" 19 | 20 | for device in devices { 21 | let row = [ 22 | device.serialNumber, 23 | device.name ?? "", 24 | device.model ?? "", 25 | device.os ?? "", 26 | device.osVersion ?? "", 27 | device.enrollmentState ?? "", 28 | device.id 29 | ] 30 | .map { field in 31 | // Escape quotes and wrap in quotes if contains comma 32 | let escaped = field.replacingOccurrences(of: "\"", with: "\"\"") 33 | return field.contains(",") ? "\"\(escaped)\"" : escaped 34 | } 35 | .joined(separator: ",") 36 | 37 | csv += row + "\n" 38 | } 39 | 40 | self.csvString = csv 41 | } 42 | 43 | init(configuration: ReadConfiguration) throws { 44 | guard let data = configuration.file.regularFileContents, 45 | let string = String(data: data, encoding: .utf8) else { 46 | throw CocoaError(.fileReadCorruptFile) 47 | } 48 | csvString = string 49 | } 50 | 51 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { 52 | let data = csvString.data(using: .utf8)! 53 | return FileWrapper(regularFileWithContents: data) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ABM-APIClient/ABMViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABMViewModel.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @MainActor 12 | class ABMViewModel: ObservableObject { 13 | @Published var devices: [OrgDevice] = [] 14 | @Published var mdmServers: [MDMServer] = [] 15 | @Published var activityStatus: ActivityStatusResponse? 16 | @Published var isLoading = false 17 | @Published var errorMessage: String? 18 | @Published var statusMessage: String? 19 | @Published var lastActivityId: String? 20 | 21 | // Credentials 22 | @Published var clientId = "" 23 | @Published var keyId = "" 24 | @Published var privateKey = "" 25 | 26 | internal let apiService = APIService() 27 | internal var clientAssertion: String? 28 | 29 | // Generate JWT 30 | func generateJWT() { 31 | isLoading = true 32 | errorMessage = nil 33 | statusMessage = nil 34 | 35 | Task { 36 | do { 37 | let credentials = APICredentials( 38 | clientId: clientId, 39 | keyId: keyId, 40 | privateKey: privateKey 41 | ) 42 | 43 | clientAssertion = try JWTGenerator.createClientAssertion(credentials: credentials) 44 | statusMessage = "JWT generated successfully" 45 | saveCredentials() 46 | } catch { 47 | errorMessage = "JWT Error: \(error.localizedDescription)" 48 | } 49 | isLoading = false 50 | } 51 | } 52 | 53 | // Fetch devices 54 | func fetchDevices() { 55 | guard let assertion = clientAssertion else { 56 | errorMessage = "Generate JWT first" 57 | return 58 | } 59 | 60 | isLoading = true 61 | errorMessage = nil 62 | statusMessage = nil 63 | 64 | Task { 65 | do { 66 | let token = try await apiService.getAccessToken( 67 | clientAssertion: assertion, 68 | clientId: clientId 69 | ) 70 | 71 | devices = try await apiService.fetchDevices(accessToken: token) 72 | statusMessage = "Fetched \(devices.count) devices" 73 | } catch { 74 | errorMessage = "API Error: \(error.localizedDescription)" 75 | } 76 | isLoading = false 77 | } 78 | } 79 | 80 | // Save credentials to UserDefaults 81 | private func saveCredentials() { 82 | UserDefaults.standard.set(clientId, forKey: "clientId") 83 | UserDefaults.standard.set(keyId, forKey: "keyId") 84 | } 85 | 86 | // Load saved credentials 87 | func loadCredentials() { 88 | clientId = UserDefaults.standard.string(forKey: "clientId") ?? "" 89 | keyId = UserDefaults.standard.string(forKey: "keyId") ?? "" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ABM-APIClient/JWTGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTGenerator.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | 9 | import Foundation 10 | import CryptoKit 11 | 12 | class JWTGenerator { 13 | static func createClientAssertion(credentials: APICredentials) throws -> String { 14 | 15 | let privateKey = try parsePrivateKey(credentials.privateKey) 16 | 17 | // Create JWT header 18 | let header = """ 19 | {"alg":"ES256","kid":"\(credentials.keyId)","typ":"JWT"} 20 | """ 21 | 22 | // Create JWT payload 23 | let now = Int(Date().timeIntervalSince1970) 24 | let exp = now + (180 * 24 * 60 * 60) // 180 days 25 | 26 | let payload = """ 27 | {"sub":"\(credentials.clientId)","aud":"https://account.apple.com/auth/oauth2/v2/token","iat":\(now),"exp":\(exp),"jti":"\(UUID().uuidString)","iss":"\(credentials.clientId)"} 28 | """ 29 | 30 | // Base64URL encode 31 | let headerBase64 = header.data(using: .utf8)!.base64URLEncoded() 32 | let payloadBase64 = payload.data(using: .utf8)!.base64URLEncoded() 33 | 34 | // Create signature 35 | let signingInput = "\(headerBase64).\(payloadBase64)" 36 | let signature = try privateKey.signature(for: signingInput.data(using: .utf8)!) 37 | 38 | // Convert signature to base64URL 39 | let signatureBase64 = signature.rawRepresentation.base64URLEncoded() 40 | 41 | return "\(signingInput).\(signatureBase64)" 42 | } 43 | 44 | private static func parsePrivateKey(_ pemString: String) throws -> P256.Signing.PrivateKey { 45 | // Remove PEM headers and whitespace 46 | let base64String = pemString 47 | .replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----", with: "") 48 | .replacingOccurrences(of: "-----END PRIVATE KEY-----", with: "") 49 | .replacingOccurrences(of: "-----BEGIN EC PRIVATE KEY-----", with: "") 50 | .replacingOccurrences(of: "-----END EC PRIVATE KEY-----", with: "") 51 | .replacingOccurrences(of: "\n", with: "") 52 | .replacingOccurrences(of: "\r", with: "") 53 | .replacingOccurrences(of: " ", with: "") 54 | 55 | guard let derData = Data(base64Encoded: base64String) else { 56 | throw NSError(domain: "JWT", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid base64"]) 57 | } 58 | 59 | 60 | if let key = try? P256.Signing.PrivateKey(derRepresentation: derData) { 61 | return key 62 | } 63 | 64 | 65 | if derData.count > 26 { 66 | // Skip PKCS#8 header for P-256 keys 67 | let keyData = derData.subdata(in: 36.. String { 84 | return self.base64EncodedString() 85 | .replacingOccurrences(of: "+", with: "-") 86 | .replacingOccurrences(of: "/", with: "_") 87 | .replacingOccurrences(of: "=", with: "") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ABM-APIClient/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | // API Credentials 11 | struct APICredentials: Codable { 12 | let clientId: String 13 | let keyId: String 14 | let privateKey: String 15 | } 16 | 17 | // Token Response 18 | struct TokenResponse: Codable { 19 | let accessToken: String 20 | let tokenType: String 21 | let expiresIn: Int 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case accessToken = "access_token" 25 | case tokenType = "token_type" 26 | case expiresIn = "expires_in" 27 | } 28 | } 29 | 30 | // Device Model 31 | struct OrgDevice: Codable, Identifiable, Hashable { 32 | let id: String 33 | let type: String 34 | let attributes: DeviceAttributes 35 | 36 | struct DeviceAttributes: Codable, Hashable { 37 | let serialNumber: String 38 | let name: String? 39 | let model: String? 40 | let deviceFamily: String? 41 | let osVersion: String? 42 | let status: String? 43 | } 44 | 45 | 46 | var serialNumber: String { attributes.serialNumber } 47 | var name: String? { attributes.name } 48 | var model: String? { attributes.model } 49 | var os: String? { attributes.deviceFamily } 50 | var osVersion: String? { attributes.osVersion } 51 | var enrollmentState: String? { attributes.status } 52 | } 53 | 54 | // Device Response 55 | struct DevicesResponse: Codable { 56 | let data: [OrgDevice] 57 | let links: Links? 58 | 59 | struct Links: Codable { 60 | let next: String? 61 | let prev: String? 62 | } 63 | } 64 | 65 | // MDM Server 66 | struct MDMServer: Codable, Identifiable, Hashable { 67 | let id: String 68 | let type: String 69 | let attributes: MDMServerAttributes 70 | 71 | struct MDMServerAttributes: Codable, Hashable { 72 | let serverName: String 73 | let serverType: String 74 | let createdDateTime: String 75 | let updatedDateTime: String 76 | } 77 | } 78 | 79 | // MDM Servers Response 80 | struct MDMServersResponse: Codable { 81 | let data: [MDMServer] 82 | let links: DevicesResponse.Links? 83 | } 84 | 85 | // Device Activity 86 | struct DeviceActivity: Codable { 87 | let data: ActivityData 88 | 89 | struct ActivityData: Codable { 90 | let type: String 91 | let attributes: ActivityAttributes 92 | let relationships: ActivityRelationships 93 | } 94 | 95 | struct ActivityAttributes: Codable { 96 | let activityType: String 97 | } 98 | 99 | struct ActivityRelationships: Codable { 100 | let mdmServer: MDMServerRelation 101 | let devices: DevicesRelation 102 | } 103 | 104 | struct MDMServerRelation: Codable { 105 | let data: RelationData 106 | } 107 | 108 | struct DevicesRelation: Codable { 109 | let data: [RelationData] 110 | } 111 | 112 | struct RelationData: Codable { 113 | let type: String 114 | let id: String 115 | } 116 | } 117 | 118 | // Activity Status Response 119 | struct ActivityStatusResponse: Codable { 120 | let data: ActivityStatus 121 | 122 | struct ActivityStatus: Codable { 123 | let id: String 124 | let type: String 125 | let attributes: StatusAttributes 126 | } 127 | 128 | struct StatusAttributes: Codable { 129 | let status: String 130 | let subStatus: String 131 | let createdDateTime: String 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ABM-APIClient/ABMViewModel+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ABMViewModel+Extensions.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ABMViewModel { 11 | // Fetch MDM servers 12 | func fetchMDMServers() { 13 | guard let assertion = clientAssertion else { 14 | errorMessage = "Generate JWT first" 15 | return 16 | } 17 | 18 | isLoading = true 19 | Task { 20 | do { 21 | let token = try await apiService.getAccessToken( 22 | clientAssertion: assertion, 23 | clientId: clientId 24 | ) 25 | mdmServers = try await apiService.fetchMDMServers(accessToken: token) 26 | statusMessage = "Fetched \(mdmServers.count) MDM servers" 27 | } catch { 28 | errorMessage = "Error: \(error.localizedDescription)" 29 | } 30 | isLoading = false 31 | } 32 | } 33 | 34 | // Get device assigned server 35 | func getDeviceAssignedServer(deviceId: String) async { 36 | guard let assertion = clientAssertion else { return } 37 | 38 | do { 39 | let token = try await apiService.getAccessToken( 40 | clientAssertion: assertion, 41 | clientId: clientId 42 | ) 43 | 44 | statusMessage = "Server info fetched" 45 | } catch { 46 | errorMessage = "Error: \(error.localizedDescription)" 47 | } 48 | } 49 | 50 | // Get devices for MDM 51 | func getDevicesForMDM(mdmId: String) async { 52 | guard let assertion = clientAssertion else { return } 53 | 54 | do { 55 | let token = try await apiService.getAccessToken( 56 | clientAssertion: assertion, 57 | clientId: clientId 58 | ) 59 | let deviceIds = try await apiService.getDevicesForMDM(mdmId: mdmId, accessToken: token) 60 | statusMessage = "MDM has \(deviceIds.count) devices" 61 | } catch { 62 | errorMessage = "Error: \(error.localizedDescription)" 63 | } 64 | } 65 | 66 | // Assign devices 67 | func assignDevices(deviceIds: [String], mdmId: String?) async { 68 | guard let assertion = clientAssertion else { return } 69 | 70 | do { 71 | let token = try await apiService.getAccessToken( 72 | clientAssertion: assertion, 73 | clientId: clientId 74 | ) 75 | let activityId = try await apiService.assignDevices( 76 | deviceIds: deviceIds, 77 | mdmId: mdmId, 78 | accessToken: token 79 | ) 80 | statusMessage = "Activity started: \(activityId)" 81 | lastActivityId = activityId 82 | } catch { 83 | errorMessage = "Error: \(error.localizedDescription)" 84 | } 85 | } 86 | 87 | // Check activity status 88 | func checkActivityStatus(activityId: String) async { 89 | guard let assertion = clientAssertion else { return } 90 | 91 | do { 92 | let token = try await apiService.getAccessToken( 93 | clientAssertion: assertion, 94 | clientId: clientId 95 | ) 96 | activityStatus = try await apiService.checkActivityStatus( 97 | activityId: activityId, 98 | accessToken: token 99 | ) 100 | statusMessage = "Status: \(activityStatus?.data.attributes.status ?? "Unknown")" 101 | } catch { 102 | errorMessage = "Error: \(error.localizedDescription)" 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ABM/ASM API Client for macOS 2 | 3 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![macOS](https://img.shields.io/badge/macOS-26+-blue?logo=apple&logoColor=white)](https://www.apple.com/macos/) 5 | [![Swift](https://img.shields.io/badge/Swift-5.10+-orange?logo=swift&logoColor=white)](https://developer.apple.com/swift/) 6 | [![Xcode](https://img.shields.io/badge/Xcode-15.0+-blue?logo=xcode&logoColor=white)](https://developer.apple.com/xcode/) 7 | 8 | A native macOS client built with Swift to provide a graphical user interface (GUI) for the new Apple Business Manager (ABM) and Apple School Manager (ASM) REST APIs. This project aims to revolutionize large-scale Apple device management by moving beyond command-line interfaces and cumbersome web portals, enabling effortless and automated workflows. 9 | 10 | ## Introduction 11 | 12 | Managing thousands of Apple devices in enterprise and education environments has historically been a manual, time-consuming process. IT administrators spend countless hours clicking through web interfaces to assign devices to MDM servers, check enrollment status, and generate reports. With Apple's introduction of REST APIs for Apple Business Manager (ABM) and Apple School Manager (ASM) at WWDC 2025, this paradigm shifts dramatically. 13 | 14 | This project presents a complete walkthrough of building a production-ready native macOS client that leverages these APIs to transform device management workflows. We'll explore the technical architecture, implementation challenges, security considerations, and real-world performance improvements achieved through API automation. 15 | 16 | ## The Problem: Manual Device Management at Scale 17 | 18 | Before we unveil the solution, let's confront the significant hurdles IT teams navigate daily when managing vast Apple device fleets: 19 | 20 | **1. Time Sinks, Not Time Savers:** 21 | * **Tedious Assignments:** Allocating 100 devices to an MDM server can consume 15-20 minutes and require roughly 300 clicks. 22 | * **Clunky Reporting:** Manual CSV exports offer limited filtering, making comprehensive reporting a slog. 23 | * **Status Quo Struggles:** Checking assignment status means navigating countless pages, one at a time. 24 | 25 | **2. The Inevitable Human Factor:** 26 | * **Misclicks & Mistakes:** Bulk selections are ripe for errors, leading to incorrect assignments. 27 | * **Inconsistent Data:** Manual processes breed variations in device naming and MDM server allocation. 28 | 29 | **3. Automation's Missing Link:** 30 | * **No Scheduled Tasks:** Routine operations demand constant manual intervention. 31 | * **Rules? What Rules?:** Lack of rule-based assignments prevents proactive management. 32 | * **Isolated Systems:** No seamless integration with essential ticketing or inventory systems. 33 | 34 | **4. Blurry Visibility:** 35 | * **Lagging Data:** Real-time status updates are a pipe dream, leaving IT in the dark. 36 | * **Basic Insights:** Reporting rarely offers the granular detail needed for informed decisions. 37 | * **Absent History:** Tracking past activities is challenging, hindering troubleshooting and auditing. 38 | 39 | ## Apple's Game-Changing API Release: Powering Our macOS Client 40 | 41 | At the heart of this revolution in device management lies Apple's new RESTful API, offering robust programmatic access to core Apple Business Manager (ABM) and Apple School Manager (ASM) functionalities. This foundational technology is what makes our native macOS client so powerful. 42 | 43 | **API Essentials:** 44 | 45 | * **Base URL:** `https://api-business.apple.com/v1/` 46 | * **Authentication:** Secure OAuth 2.0 with JWT Client Assertions. 47 | * **Rate Limits:** A generous 100 requests per second. 48 | * **Pagination:** Efficient link-based pagination, defaulting to 100 items per page. 49 | 50 | **Key Endpoints for Comprehensive Control:** 51 | 52 | The API exposes critical endpoints, enabling the granular device management capabilities within our client: 53 | 54 | * **Device Information:** 55 | * `GET /v1/orgDevices` — List all organizational devices. 56 | * `GET /v1/orgDevices/{id}` — Get detailed information for a specific device. 57 | * `GET /v1/orgDevices/{id}/relationships/assignedServer` — Determine a device's assigned MDM server. 58 | * **MDM Server Operations:** 59 | * `GET /v1/mdmServers` — Retrieve a list of all registered MDM servers. 60 | * `GET /v1/mdmServers/{id}/relationships/devices` — View devices associated with a particular MDM server. 61 | * **Powerful Batch Operations:** 62 | * `POST /v1/orgDeviceActivities` — Execute bulk assignments or unassignments of devices. 63 | * `GET /v1/orgDeviceActivities/{id}` — Monitor the status of ongoing batch operations. 64 | 65 | ## Authentication Flow: JWT-Based Security 66 | 67 | Apple's API authentication moves beyond simple API keys, leveraging a more secure and robust JWT (JSON Web Token) based mechanism. Here's a breakdown of the multi-step process: 68 | 69 | **1. Initial Setup in ABM:** 70 | 71 | * **Create an API Key:** Begin by generating an API key directly within your Apple Business Manager account. 72 | * **Generate Private Key:** During this process, you'll generate a private key (using the P-256 elliptic curve), which is crucial for signing your JWTs. 73 | * **Note Credentials:** Securely record your unique **Client ID** and **Key ID** — these identify your application. 74 | * **Secure Storage:** Crucially, the generated private key must be stored in a highly secure manner. 75 | 76 | **2. Generating the JWT Client Assertion:** 77 | 78 | This is the signed token your application creates to assert its identity. It consists of a header and a payload: 79 | 80 | ```json 81 | // JWT Header: Specifies the algorithm and key ID 82 | { 83 | "alg": "ES256", // Elliptic Curve Digital Signature Algorithm using P-256 and SHA-256 84 | "kid": "your-key-id", // Your unique Key ID from ABM 85 | "typ": "JWT" // Token type 86 | } 87 | 88 | // JWT Payload: Contains claims about the assertion 89 | { 90 | "sub": "BUSINESSAPI.client-id", // The subject (your Client ID, prefixed) 91 | "aud": "[https://account.apple.com/auth/oauth2/v2/token](https://account.apple.com/auth/oauth2/v2/token)", // The audience (Apple's token endpoint) 92 | "iat": 1719263110, // Issued At timestamp (Unix epoch time) 93 | "exp": 1734815110, // Expiration timestamp (Unix epoch time) 94 | "jti": "unique-token-id", // Unique JWT ID for replay prevention 95 | "iss": "BUSINESSAPI.client-id" // The issuer (your Client ID, prefixed) 96 | } 97 | -------------------------------------------------------------------------------- /ABM-APIClient/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | class APIService { 12 | private var accessToken: String? 13 | private var tokenExpiry: Date? 14 | 15 | // Get access token 16 | func getAccessToken(clientAssertion: String, clientId: String) async throws -> String { 17 | // Check if we have valid token 18 | if let token = accessToken, let expiry = tokenExpiry, expiry > Date() { 19 | return token 20 | } 21 | 22 | // Create request 23 | var request = URLRequest(url: URL(string: "https://account.apple.com/auth/oauth2/token")!) 24 | request.httpMethod = "POST" 25 | request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") 26 | 27 | // Create body 28 | let bodyParams = [ 29 | "grant_type": "client_credentials", 30 | "client_id": clientId, 31 | "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 32 | "client_assertion": clientAssertion, 33 | "scope": "business.api" 34 | ] 35 | 36 | let bodyString = bodyParams 37 | .map { "\($0.key)=\($0.value)" } 38 | .joined(separator: "&") 39 | 40 | request.httpBody = bodyString.data(using: .utf8) 41 | 42 | // Make request 43 | let (data, _) = try await URLSession.shared.data(for: request) 44 | let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data) 45 | 46 | // Store token and expiry 47 | self.accessToken = tokenResponse.accessToken 48 | self.tokenExpiry = Date().addingTimeInterval(TimeInterval(tokenResponse.expiresIn)) 49 | 50 | return tokenResponse.accessToken 51 | } 52 | 53 | // Check activity status 54 | func checkActivityStatus(activityId: String, accessToken: String) async throws -> ActivityStatusResponse { 55 | let url = URL(string: "https://api-business.apple.com/v1/orgDeviceActivities/\(activityId)")! 56 | var request = URLRequest(url: url) 57 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 58 | 59 | let (data, _) = try await URLSession.shared.data(for: request) 60 | return try JSONDecoder().decode(ActivityStatusResponse.self, from: data) 61 | } 62 | 63 | // Fetch devices 64 | func fetchDevices(accessToken: String) async throws -> [OrgDevice] { 65 | var allDevices: [OrgDevice] = [] 66 | var nextURL: String? = "https://api-business.apple.com/v1/orgDevices" 67 | 68 | while let urlString = nextURL { 69 | guard let url = URL(string: urlString) else { break } 70 | 71 | var request = URLRequest(url: url) 72 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 73 | request.setValue("application/json", forHTTPHeaderField: "Accept") 74 | 75 | let (data, response) = try await URLSession.shared.data(for: request) 76 | 77 | // Debug response 78 | if let httpResponse = response as? HTTPURLResponse { 79 | print("Status Code: \(httpResponse.statusCode)") 80 | print("Headers: \(httpResponse.allHeaderFields)") 81 | if httpResponse.statusCode != 200 { 82 | print("Response: \(String(data: data, encoding: .utf8) ?? "No data")") 83 | throw NSError(domain: "API", code: httpResponse.statusCode, 84 | userInfo: [NSLocalizedDescriptionKey: "API returned status \(httpResponse.statusCode)"]) 85 | } 86 | } 87 | 88 | let deviceResponse = try JSONDecoder().decode(DevicesResponse.self, from: data) 89 | 90 | allDevices.append(contentsOf: deviceResponse.data) 91 | nextURL = deviceResponse.links?.next 92 | } 93 | 94 | return allDevices 95 | } 96 | 97 | // Get device by ID 98 | func getDevice(id: String, accessToken: String) async throws -> OrgDevice { 99 | let url = URL(string: "https://api-business.apple.com/v1/orgDevices/\(id)")! 100 | var request = URLRequest(url: url) 101 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 102 | 103 | let (data, _) = try await URLSession.shared.data(for: request) 104 | let response = try JSONDecoder().decode(DeviceDetailResponse.self, from: data) 105 | return response.data 106 | } 107 | 108 | // Fetch MDM servers 109 | func fetchMDMServers(accessToken: String) async throws -> [MDMServer] { 110 | let url = URL(string: "https://api-business.apple.com/v1/mdmServers")! 111 | var request = URLRequest(url: url) 112 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 113 | request.setValue("application/json", forHTTPHeaderField: "Accept") 114 | 115 | let (data, response) = try await URLSession.shared.data(for: request) 116 | 117 | if let httpResponse = response as? HTTPURLResponse { 118 | print("MDM Status Code: \(httpResponse.statusCode)") 119 | if httpResponse.statusCode != 200 { 120 | print("MDM Response: \(String(data: data, encoding: .utf8) ?? "No data")") 121 | } 122 | } 123 | 124 | let mdmResponse = try JSONDecoder().decode(MDMServersResponse.self, from: data) 125 | return mdmResponse.data 126 | } 127 | 128 | // Get devices for MDM server 129 | func getDevicesForMDM(mdmId: String, accessToken: String) async throws -> [String] { 130 | let url = URL(string: "https://api-business.apple.com/v1/mdmServers/\(mdmId)/relationships/devices")! 131 | var request = URLRequest(url: url) 132 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 133 | 134 | let (data, _) = try await URLSession.shared.data(for: request) 135 | let response = try JSONDecoder().decode(RelationshipResponse.self, from: data) 136 | return response.data.map { $0.id } 137 | } 138 | 139 | // Assign/unassign devices 140 | func assignDevices(deviceIds: [String], mdmId: String?, accessToken: String) async throws -> String { 141 | let url = URL(string: "https://api-business.apple.com/v1/orgDeviceActivities")! 142 | var request = URLRequest(url: url) 143 | request.httpMethod = "POST" 144 | request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 145 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 146 | 147 | let activity = DeviceActivity( 148 | data: DeviceActivity.ActivityData( 149 | type: "orgDeviceActivities", 150 | attributes: DeviceActivity.ActivityAttributes( 151 | activityType: mdmId != nil ? "ASSIGN_DEVICES" : "UNASSIGN_DEVICES" 152 | ), 153 | relationships: DeviceActivity.ActivityRelationships( 154 | mdmServer: DeviceActivity.MDMServerRelation( 155 | data: DeviceActivity.RelationData( 156 | type: "mdmServers", 157 | id: mdmId ?? "" 158 | ) 159 | ), 160 | devices: DeviceActivity.DevicesRelation( 161 | data: deviceIds.map { 162 | DeviceActivity.RelationData(type: "orgDevices", id: $0) 163 | } 164 | ) 165 | ) 166 | ) 167 | ) 168 | 169 | request.httpBody = try JSONEncoder().encode(activity) 170 | 171 | let (data, response) = try await URLSession.shared.data(for: request) 172 | 173 | if let httpResponse = response as? HTTPURLResponse { 174 | print("Assign Status: \(httpResponse.statusCode)") 175 | if httpResponse.statusCode != 200 && httpResponse.statusCode != 201 { 176 | print("Assign Error: \(String(data: data, encoding: .utf8) ?? "")") 177 | throw NSError(domain: "API", code: httpResponse.statusCode) 178 | } 179 | } 180 | 181 | let activityResponse = try JSONDecoder().decode(ActivityStatusResponse.self, from: data) 182 | return activityResponse.data.id 183 | } 184 | } 185 | 186 | 187 | struct DeviceDetailResponse: Codable { 188 | let data: OrgDevice 189 | } 190 | 191 | struct RelationshipResponse: Codable { 192 | let data: [RelationData] 193 | 194 | struct RelationData: Codable { 195 | let type: String 196 | let id: String 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /ABM-APIClient.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXContainerItemProxy section */ 10 | FB4421092E09F3040013CFC0 /* PBXContainerItemProxy */ = { 11 | isa = PBXContainerItemProxy; 12 | containerPortal = FB4420F22E09F3030013CFC0 /* Project object */; 13 | proxyType = 1; 14 | remoteGlobalIDString = FB4420F92E09F3030013CFC0; 15 | remoteInfo = "ABM-APIClient"; 16 | }; 17 | FB4421132E09F3040013CFC0 /* PBXContainerItemProxy */ = { 18 | isa = PBXContainerItemProxy; 19 | containerPortal = FB4420F22E09F3030013CFC0 /* Project object */; 20 | proxyType = 1; 21 | remoteGlobalIDString = FB4420F92E09F3030013CFC0; 22 | remoteInfo = "ABM-APIClient"; 23 | }; 24 | /* End PBXContainerItemProxy section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | FB4420FA2E09F3030013CFC0 /* ABM-APIClient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ABM-APIClient.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | FB4421082E09F3040013CFC0 /* ABM-APIClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ABM-APIClientTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | FB4421122E09F3040013CFC0 /* ABM-APIClientUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ABM-APIClientUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 33 | FB4421312E09F4F40013CFC0 /* Exceptions for "ABM-APIClient" folder in "ABM-APIClient" target */ = { 34 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 35 | membershipExceptions = ( 36 | Info.plist, 37 | ); 38 | target = FB4420F92E09F3030013CFC0 /* ABM-APIClient */; 39 | }; 40 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 41 | 42 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 43 | FB4420FC2E09F3030013CFC0 /* ABM-APIClient */ = { 44 | isa = PBXFileSystemSynchronizedRootGroup; 45 | exceptions = ( 46 | FB4421312E09F4F40013CFC0 /* Exceptions for "ABM-APIClient" folder in "ABM-APIClient" target */, 47 | ); 48 | path = "ABM-APIClient"; 49 | sourceTree = ""; 50 | }; 51 | FB44210B2E09F3040013CFC0 /* ABM-APIClientTests */ = { 52 | isa = PBXFileSystemSynchronizedRootGroup; 53 | path = "ABM-APIClientTests"; 54 | sourceTree = ""; 55 | }; 56 | FB4421152E09F3040013CFC0 /* ABM-APIClientUITests */ = { 57 | isa = PBXFileSystemSynchronizedRootGroup; 58 | path = "ABM-APIClientUITests"; 59 | sourceTree = ""; 60 | }; 61 | /* End PBXFileSystemSynchronizedRootGroup section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | FB4420F72E09F3030013CFC0 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | FB4421052E09F3040013CFC0 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | FB44210F2E09F3040013CFC0 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | FB4420F12E09F3030013CFC0 = { 89 | isa = PBXGroup; 90 | children = ( 91 | FB4420FC2E09F3030013CFC0 /* ABM-APIClient */, 92 | FB44210B2E09F3040013CFC0 /* ABM-APIClientTests */, 93 | FB4421152E09F3040013CFC0 /* ABM-APIClientUITests */, 94 | FB4420FB2E09F3030013CFC0 /* Products */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | FB4420FB2E09F3030013CFC0 /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | FB4420FA2E09F3030013CFC0 /* ABM-APIClient.app */, 102 | FB4421082E09F3040013CFC0 /* ABM-APIClientTests.xctest */, 103 | FB4421122E09F3040013CFC0 /* ABM-APIClientUITests.xctest */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | FB4420F92E09F3030013CFC0 /* ABM-APIClient */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = FB44211C2E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClient" */; 114 | buildPhases = ( 115 | FB4420F62E09F3030013CFC0 /* Sources */, 116 | FB4420F72E09F3030013CFC0 /* Frameworks */, 117 | FB4420F82E09F3030013CFC0 /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | fileSystemSynchronizedGroups = ( 124 | FB4420FC2E09F3030013CFC0 /* ABM-APIClient */, 125 | ); 126 | name = "ABM-APIClient"; 127 | packageProductDependencies = ( 128 | ); 129 | productName = "ABM-APIClient"; 130 | productReference = FB4420FA2E09F3030013CFC0 /* ABM-APIClient.app */; 131 | productType = "com.apple.product-type.application"; 132 | }; 133 | FB4421072E09F3040013CFC0 /* ABM-APIClientTests */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = FB44211F2E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClientTests" */; 136 | buildPhases = ( 137 | FB4421042E09F3040013CFC0 /* Sources */, 138 | FB4421052E09F3040013CFC0 /* Frameworks */, 139 | FB4421062E09F3040013CFC0 /* Resources */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | FB44210A2E09F3040013CFC0 /* PBXTargetDependency */, 145 | ); 146 | fileSystemSynchronizedGroups = ( 147 | FB44210B2E09F3040013CFC0 /* ABM-APIClientTests */, 148 | ); 149 | name = "ABM-APIClientTests"; 150 | packageProductDependencies = ( 151 | ); 152 | productName = "ABM-APIClientTests"; 153 | productReference = FB4421082E09F3040013CFC0 /* ABM-APIClientTests.xctest */; 154 | productType = "com.apple.product-type.bundle.unit-test"; 155 | }; 156 | FB4421112E09F3040013CFC0 /* ABM-APIClientUITests */ = { 157 | isa = PBXNativeTarget; 158 | buildConfigurationList = FB4421222E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClientUITests" */; 159 | buildPhases = ( 160 | FB44210E2E09F3040013CFC0 /* Sources */, 161 | FB44210F2E09F3040013CFC0 /* Frameworks */, 162 | FB4421102E09F3040013CFC0 /* Resources */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | FB4421142E09F3040013CFC0 /* PBXTargetDependency */, 168 | ); 169 | fileSystemSynchronizedGroups = ( 170 | FB4421152E09F3040013CFC0 /* ABM-APIClientUITests */, 171 | ); 172 | name = "ABM-APIClientUITests"; 173 | packageProductDependencies = ( 174 | ); 175 | productName = "ABM-APIClientUITests"; 176 | productReference = FB4421122E09F3040013CFC0 /* ABM-APIClientUITests.xctest */; 177 | productType = "com.apple.product-type.bundle.ui-testing"; 178 | }; 179 | /* End PBXNativeTarget section */ 180 | 181 | /* Begin PBXProject section */ 182 | FB4420F22E09F3030013CFC0 /* Project object */ = { 183 | isa = PBXProject; 184 | attributes = { 185 | BuildIndependentTargetsInParallel = 1; 186 | LastSwiftUpdateCheck = 1640; 187 | LastUpgradeCheck = 1640; 188 | TargetAttributes = { 189 | FB4420F92E09F3030013CFC0 = { 190 | CreatedOnToolsVersion = 16.4; 191 | }; 192 | FB4421072E09F3040013CFC0 = { 193 | CreatedOnToolsVersion = 16.4; 194 | TestTargetID = FB4420F92E09F3030013CFC0; 195 | }; 196 | FB4421112E09F3040013CFC0 = { 197 | CreatedOnToolsVersion = 16.4; 198 | TestTargetID = FB4420F92E09F3030013CFC0; 199 | }; 200 | }; 201 | }; 202 | buildConfigurationList = FB4420F52E09F3030013CFC0 /* Build configuration list for PBXProject "ABM-APIClient" */; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = FB4420F12E09F3030013CFC0; 210 | minimizedProjectReferenceProxies = 1; 211 | packageReferences = ( 212 | FB4421252E09F37B0013CFC0 /* XCRemoteSwiftPackageReference "jwt-kit" */, 213 | FB4421492E09F6360013CFC0 /* XCRemoteSwiftPackageReference "Swift-JWT" */, 214 | ); 215 | preferredProjectObjectVersion = 77; 216 | productRefGroup = FB4420FB2E09F3030013CFC0 /* Products */; 217 | projectDirPath = ""; 218 | projectRoot = ""; 219 | targets = ( 220 | FB4420F92E09F3030013CFC0 /* ABM-APIClient */, 221 | FB4421072E09F3040013CFC0 /* ABM-APIClientTests */, 222 | FB4421112E09F3040013CFC0 /* ABM-APIClientUITests */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | FB4420F82E09F3030013CFC0 /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | FB4421062E09F3040013CFC0 /* Resources */ = { 236 | isa = PBXResourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | FB4421102E09F3040013CFC0 /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXResourcesBuildPhase section */ 250 | 251 | /* Begin PBXSourcesBuildPhase section */ 252 | FB4420F62E09F3030013CFC0 /* Sources */ = { 253 | isa = PBXSourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | FB4421042E09F3040013CFC0 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | FB44210E2E09F3040013CFC0 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | /* End PBXSourcesBuildPhase section */ 274 | 275 | /* Begin PBXTargetDependency section */ 276 | FB44210A2E09F3040013CFC0 /* PBXTargetDependency */ = { 277 | isa = PBXTargetDependency; 278 | target = FB4420F92E09F3030013CFC0 /* ABM-APIClient */; 279 | targetProxy = FB4421092E09F3040013CFC0 /* PBXContainerItemProxy */; 280 | }; 281 | FB4421142E09F3040013CFC0 /* PBXTargetDependency */ = { 282 | isa = PBXTargetDependency; 283 | target = FB4420F92E09F3030013CFC0 /* ABM-APIClient */; 284 | targetProxy = FB4421132E09F3040013CFC0 /* PBXContainerItemProxy */; 285 | }; 286 | /* End PBXTargetDependency section */ 287 | 288 | /* Begin XCBuildConfiguration section */ 289 | FB44211A2E09F3040013CFC0 /* Debug */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ALWAYS_SEARCH_USER_PATHS = NO; 293 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 294 | CLANG_ANALYZER_NONNULL = YES; 295 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 296 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 297 | CLANG_ENABLE_MODULES = YES; 298 | CLANG_ENABLE_OBJC_ARC = YES; 299 | CLANG_ENABLE_OBJC_WEAK = YES; 300 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 301 | CLANG_WARN_BOOL_CONVERSION = YES; 302 | CLANG_WARN_COMMA = YES; 303 | CLANG_WARN_CONSTANT_CONVERSION = YES; 304 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 305 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 306 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 307 | CLANG_WARN_EMPTY_BODY = YES; 308 | CLANG_WARN_ENUM_CONVERSION = YES; 309 | CLANG_WARN_INFINITE_RECURSION = YES; 310 | CLANG_WARN_INT_CONVERSION = YES; 311 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 313 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 315 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 316 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 317 | CLANG_WARN_STRICT_PROTOTYPES = YES; 318 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 319 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 320 | CLANG_WARN_UNREACHABLE_CODE = YES; 321 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 322 | COPY_PHASE_STRIP = NO; 323 | DEBUG_INFORMATION_FORMAT = dwarf; 324 | DEVELOPMENT_TEAM = LJ3W53UDG4; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | ENABLE_TESTABILITY = YES; 327 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu17; 329 | GCC_DYNAMIC_NO_PIC = NO; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_OPTIMIZATION_LEVEL = 0; 332 | GCC_PREPROCESSOR_DEFINITIONS = ( 333 | "DEBUG=1", 334 | "$(inherited)", 335 | ); 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 343 | MACOSX_DEPLOYMENT_TARGET = 15.5; 344 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 345 | MTL_FAST_MATH = YES; 346 | ONLY_ACTIVE_ARCH = YES; 347 | SDKROOT = macosx; 348 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 349 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 350 | }; 351 | name = Debug; 352 | }; 353 | FB44211B2E09F3040013CFC0 /* Release */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ALWAYS_SEARCH_USER_PATHS = NO; 357 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 358 | CLANG_ANALYZER_NONNULL = YES; 359 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 360 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_ENABLE_OBJC_WEAK = YES; 364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_COMMA = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 370 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 371 | CLANG_WARN_EMPTY_BODY = YES; 372 | CLANG_WARN_ENUM_CONVERSION = YES; 373 | CLANG_WARN_INFINITE_RECURSION = YES; 374 | CLANG_WARN_INT_CONVERSION = YES; 375 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 377 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 379 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 388 | DEVELOPMENT_TEAM = LJ3W53UDG4; 389 | ENABLE_NS_ASSERTIONS = NO; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 392 | GCC_C_LANGUAGE_STANDARD = gnu17; 393 | GCC_NO_COMMON_BLOCKS = YES; 394 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 395 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 398 | GCC_WARN_UNUSED_FUNCTION = YES; 399 | GCC_WARN_UNUSED_VARIABLE = YES; 400 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 401 | MACOSX_DEPLOYMENT_TARGET = 15.5; 402 | MTL_ENABLE_DEBUG_INFO = NO; 403 | MTL_FAST_MATH = YES; 404 | SDKROOT = macosx; 405 | SWIFT_COMPILATION_MODE = wholemodule; 406 | }; 407 | name = Release; 408 | }; 409 | FB44211D2E09F3040013CFC0 /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 414 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 415 | CODE_SIGN_ENTITLEMENTS = "ABM-APIClient/ABM_APIClient.entitlements"; 416 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 417 | CODE_SIGN_STYLE = Automatic; 418 | COMBINE_HIDPI_IMAGES = YES; 419 | CURRENT_PROJECT_VERSION = 1; 420 | DEVELOPMENT_TEAM = LJ3W53UDG4; 421 | ENABLE_HARDENED_RUNTIME = YES; 422 | ENABLE_PREVIEWS = YES; 423 | GENERATE_INFOPLIST_FILE = YES; 424 | INFOPLIST_FILE = "ABM-APIClient/Info.plist"; 425 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 426 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 427 | LD_RUNPATH_SEARCH_PATHS = ( 428 | "$(inherited)", 429 | "@executable_path/../Frameworks", 430 | ); 431 | MARKETING_VERSION = 1.0; 432 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClient"; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | REGISTER_APP_GROUPS = YES; 435 | SWIFT_EMIT_LOC_STRINGS = YES; 436 | SWIFT_VERSION = 5.0; 437 | }; 438 | name = Debug; 439 | }; 440 | FB44211E2E09F3040013CFC0 /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 445 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 446 | CODE_SIGN_ENTITLEMENTS = "ABM-APIClient/ABM_APIClient.entitlements"; 447 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 448 | CODE_SIGN_STYLE = Automatic; 449 | COMBINE_HIDPI_IMAGES = YES; 450 | CURRENT_PROJECT_VERSION = 1; 451 | DEVELOPMENT_TEAM = LJ3W53UDG4; 452 | ENABLE_HARDENED_RUNTIME = YES; 453 | ENABLE_PREVIEWS = YES; 454 | GENERATE_INFOPLIST_FILE = YES; 455 | INFOPLIST_FILE = "ABM-APIClient/Info.plist"; 456 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 457 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 458 | LD_RUNPATH_SEARCH_PATHS = ( 459 | "$(inherited)", 460 | "@executable_path/../Frameworks", 461 | ); 462 | MARKETING_VERSION = 1.0; 463 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClient"; 464 | PRODUCT_NAME = "$(TARGET_NAME)"; 465 | REGISTER_APP_GROUPS = YES; 466 | SWIFT_EMIT_LOC_STRINGS = YES; 467 | SWIFT_VERSION = 5.0; 468 | }; 469 | name = Release; 470 | }; 471 | FB4421202E09F3040013CFC0 /* Debug */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | BUNDLE_LOADER = "$(TEST_HOST)"; 475 | CODE_SIGN_STYLE = Automatic; 476 | CURRENT_PROJECT_VERSION = 1; 477 | DEVELOPMENT_TEAM = LJ3W53UDG4; 478 | GENERATE_INFOPLIST_FILE = YES; 479 | MACOSX_DEPLOYMENT_TARGET = 15.5; 480 | MARKETING_VERSION = 1.0; 481 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClientTests"; 482 | PRODUCT_NAME = "$(TARGET_NAME)"; 483 | SWIFT_EMIT_LOC_STRINGS = NO; 484 | SWIFT_VERSION = 5.0; 485 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ABM-APIClient.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ABM-APIClient"; 486 | }; 487 | name = Debug; 488 | }; 489 | FB4421212E09F3040013CFC0 /* Release */ = { 490 | isa = XCBuildConfiguration; 491 | buildSettings = { 492 | BUNDLE_LOADER = "$(TEST_HOST)"; 493 | CODE_SIGN_STYLE = Automatic; 494 | CURRENT_PROJECT_VERSION = 1; 495 | DEVELOPMENT_TEAM = LJ3W53UDG4; 496 | GENERATE_INFOPLIST_FILE = YES; 497 | MACOSX_DEPLOYMENT_TARGET = 15.5; 498 | MARKETING_VERSION = 1.0; 499 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClientTests"; 500 | PRODUCT_NAME = "$(TARGET_NAME)"; 501 | SWIFT_EMIT_LOC_STRINGS = NO; 502 | SWIFT_VERSION = 5.0; 503 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ABM-APIClient.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ABM-APIClient"; 504 | }; 505 | name = Release; 506 | }; 507 | FB4421232E09F3040013CFC0 /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | CODE_SIGN_STYLE = Automatic; 511 | CURRENT_PROJECT_VERSION = 1; 512 | DEVELOPMENT_TEAM = LJ3W53UDG4; 513 | GENERATE_INFOPLIST_FILE = YES; 514 | MARKETING_VERSION = 1.0; 515 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClientUITests"; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | SWIFT_EMIT_LOC_STRINGS = NO; 518 | SWIFT_VERSION = 5.0; 519 | TEST_TARGET_NAME = "ABM-APIClient"; 520 | }; 521 | name = Debug; 522 | }; 523 | FB4421242E09F3040013CFC0 /* Release */ = { 524 | isa = XCBuildConfiguration; 525 | buildSettings = { 526 | CODE_SIGN_STYLE = Automatic; 527 | CURRENT_PROJECT_VERSION = 1; 528 | DEVELOPMENT_TEAM = LJ3W53UDG4; 529 | GENERATE_INFOPLIST_FILE = YES; 530 | MARKETING_VERSION = 1.0; 531 | PRODUCT_BUNDLE_IDENTIFIER = "com.mavericklabs.ABM-APIClientUITests"; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SWIFT_EMIT_LOC_STRINGS = NO; 534 | SWIFT_VERSION = 5.0; 535 | TEST_TARGET_NAME = "ABM-APIClient"; 536 | }; 537 | name = Release; 538 | }; 539 | /* End XCBuildConfiguration section */ 540 | 541 | /* Begin XCConfigurationList section */ 542 | FB4420F52E09F3030013CFC0 /* Build configuration list for PBXProject "ABM-APIClient" */ = { 543 | isa = XCConfigurationList; 544 | buildConfigurations = ( 545 | FB44211A2E09F3040013CFC0 /* Debug */, 546 | FB44211B2E09F3040013CFC0 /* Release */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | defaultConfigurationName = Release; 550 | }; 551 | FB44211C2E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClient" */ = { 552 | isa = XCConfigurationList; 553 | buildConfigurations = ( 554 | FB44211D2E09F3040013CFC0 /* Debug */, 555 | FB44211E2E09F3040013CFC0 /* Release */, 556 | ); 557 | defaultConfigurationIsVisible = 0; 558 | defaultConfigurationName = Release; 559 | }; 560 | FB44211F2E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClientTests" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | FB4421202E09F3040013CFC0 /* Debug */, 564 | FB4421212E09F3040013CFC0 /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | FB4421222E09F3040013CFC0 /* Build configuration list for PBXNativeTarget "ABM-APIClientUITests" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | FB4421232E09F3040013CFC0 /* Debug */, 573 | FB4421242E09F3040013CFC0 /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | /* End XCConfigurationList section */ 579 | 580 | /* Begin XCRemoteSwiftPackageReference section */ 581 | FB4421252E09F37B0013CFC0 /* XCRemoteSwiftPackageReference "jwt-kit" */ = { 582 | isa = XCRemoteSwiftPackageReference; 583 | repositoryURL = "https://github.com/vapor/jwt-kit.git"; 584 | requirement = { 585 | kind = upToNextMajorVersion; 586 | minimumVersion = 5.1.2; 587 | }; 588 | }; 589 | FB4421492E09F6360013CFC0 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = { 590 | isa = XCRemoteSwiftPackageReference; 591 | repositoryURL = "https://github.com/Kitura/Swift-JWT.git"; 592 | requirement = { 593 | kind = upToNextMajorVersion; 594 | minimumVersion = 4.0.2; 595 | }; 596 | }; 597 | /* End XCRemoteSwiftPackageReference section */ 598 | }; 599 | rootObject = FB4420F22E09F3030013CFC0 /* Project object */; 600 | } 601 | -------------------------------------------------------------------------------- /ABM-APIClient/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ABM-APIClient 4 | // 5 | // © Created by Somesh Pathak on 23/06/2025. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | struct ContentView: View { 12 | @StateObject private var viewModel = ABMViewModel() 13 | @State private var showingPrivateKeyInput = false 14 | @State private var selectedTab = 0 15 | 16 | var body: some View { 17 | NavigationSplitView { 18 | // Sidebar - Credentials 19 | VStack(spacing: 0) { 20 | // Header 21 | VStack(spacing: 16) { 22 | HStack { 23 | Image(systemName: "network") 24 | .font(.largeTitle) 25 | .foregroundColor(.blue) 26 | VStack(alignment: .leading) { 27 | Text("ABM API Client") 28 | .font(.headline) 29 | Text("Manage your devices") 30 | .font(.caption) 31 | .foregroundColor(.secondary) 32 | } 33 | Spacer() 34 | } 35 | .padding(.bottom, 8) 36 | 37 | Divider() 38 | } 39 | .padding() 40 | 41 | // Credentials Form 42 | VStack(alignment: .leading, spacing: 20) { 43 | Label("API Credentials", systemImage: "key.fill") 44 | .font(.headline) 45 | .foregroundColor(.secondary) 46 | 47 | VStack(alignment: .leading, spacing: 16) { 48 | VStack(alignment: .leading, spacing: 4) { 49 | Label("Client ID", systemImage: "person.circle") 50 | .font(.caption) 51 | .foregroundColor(.secondary) 52 | TextField("BUSINESSAPI.xxx", text: $viewModel.clientId) 53 | .textFieldStyle(.roundedBorder) 54 | } 55 | 56 | VStack(alignment: .leading, spacing: 4) { 57 | Label("Key ID", systemImage: "key") 58 | .font(.caption) 59 | .foregroundColor(.secondary) 60 | TextField("d136aa66-xxx", text: $viewModel.keyId) 61 | .textFieldStyle(.roundedBorder) 62 | } 63 | 64 | VStack(alignment: .leading, spacing: 4) { 65 | Label("Private Key", systemImage: "lock.fill") 66 | .font(.caption) 67 | .foregroundColor(.secondary) 68 | Button(action: { showingPrivateKeyInput = true }) { 69 | HStack { 70 | Image(systemName: viewModel.privateKey.isEmpty ? "doc.badge.plus" : "checkmark.circle.fill") 71 | .foregroundColor(viewModel.privateKey.isEmpty ? .secondary : .green) 72 | Text(viewModel.privateKey.isEmpty ? "Load Private Key File" : "Private Key Loaded") 73 | Spacer() 74 | } 75 | .padding(8) 76 | .background(Color.gray.opacity(0.1)) 77 | .cornerRadius(6) 78 | } 79 | .buttonStyle(.plain) 80 | } 81 | } 82 | 83 | Divider() 84 | 85 | // Action Buttons 86 | VStack(spacing: 12) { 87 | Button(action: { viewModel.generateJWT() }) { 88 | HStack { 89 | Image(systemName: "lock.rotation") 90 | Text("Generate JWT") 91 | Spacer() 92 | } 93 | } 94 | .buttonStyle(.borderedProminent) 95 | .controlSize(.large) 96 | .disabled(viewModel.clientId.isEmpty || viewModel.keyId.isEmpty || viewModel.privateKey.isEmpty) 97 | 98 | Button(action: { 99 | viewModel.fetchDevices() 100 | viewModel.fetchMDMServers() 101 | }) { 102 | HStack { 103 | Image(systemName: "arrow.triangle.2.circlepath") 104 | Text("Connect to ABM") 105 | Spacer() 106 | } 107 | } 108 | .buttonStyle(.bordered) 109 | .controlSize(.large) 110 | .disabled(viewModel.clientAssertion == nil) 111 | } 112 | 113 | // Status Section 114 | if viewModel.isLoading || viewModel.statusMessage != nil || viewModel.errorMessage != nil { 115 | Divider() 116 | 117 | VStack(alignment: .leading, spacing: 8) { 118 | if viewModel.isLoading { 119 | HStack { 120 | ProgressView() 121 | .progressViewStyle(CircularProgressViewStyle()) 122 | .scaleEffect(0.8) 123 | Text("Loading...") 124 | .font(.caption) 125 | } 126 | } 127 | 128 | if let status = viewModel.statusMessage { 129 | HStack { 130 | Image(systemName: "checkmark.circle") 131 | .foregroundColor(.green) 132 | Text(status) 133 | .font(.caption) 134 | } 135 | } 136 | 137 | if let error = viewModel.errorMessage { 138 | HStack(alignment: .top) { 139 | Image(systemName: "exclamationmark.triangle") 140 | .foregroundColor(.red) 141 | Text(error) 142 | .font(.caption) 143 | .fixedSize(horizontal: false, vertical: true) 144 | } 145 | } 146 | } 147 | .padding(12) 148 | .background(Color.gray.opacity(0.05)) 149 | .cornerRadius(8) 150 | } 151 | 152 | Spacer() 153 | } 154 | .padding() 155 | } 156 | .frame(minWidth: 320, idealWidth: 350) 157 | .background(Color(NSColor.controlBackgroundColor)) 158 | } detail: { 159 | // Main content with tabs 160 | TabView(selection: $selectedTab) { 161 | // Devices Tab 162 | DevicesView(viewModel: viewModel) 163 | .tabItem { 164 | Label("Devices", systemImage: "desktopcomputer") 165 | } 166 | .tag(0) 167 | 168 | // MDM Servers Tab 169 | MDMServersView(viewModel: viewModel) 170 | .tabItem { 171 | Label("MDM Servers", systemImage: "server.rack") 172 | } 173 | .tag(1) 174 | 175 | // Device Assignment Tab 176 | DeviceAssignmentView(viewModel: viewModel) 177 | .tabItem { 178 | Label("Assign Devices", systemImage: "arrow.right.square") 179 | } 180 | .tag(2) 181 | 182 | // Activity Status Tab 183 | ActivityStatusView(viewModel: viewModel) 184 | .tabItem { 185 | Label("Activity Status", systemImage: "clock.arrow.circlepath") 186 | } 187 | .tag(3) 188 | } 189 | .frame(minWidth: 800, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) 190 | } 191 | .onAppear { 192 | viewModel.loadCredentials() 193 | } 194 | .fileImporter( 195 | isPresented: $showingPrivateKeyInput, 196 | allowedContentTypes: [.plainText, .item], 197 | allowsMultipleSelection: false 198 | ) { result in 199 | switch result { 200 | case .success(let files): 201 | if let file = files.first { 202 | loadPrivateKey(from: file) 203 | } 204 | case .failure(let error): 205 | viewModel.errorMessage = "Failed to load key: \(error.localizedDescription)" 206 | } 207 | } 208 | } 209 | 210 | private func loadPrivateKey(from url: URL) { 211 | if url.startAccessingSecurityScopedResource() { 212 | defer { url.stopAccessingSecurityScopedResource() } 213 | 214 | do { 215 | viewModel.privateKey = try String(contentsOf: url, encoding: .utf8) 216 | } catch { 217 | viewModel.errorMessage = "Failed to read key: \(error.localizedDescription)" 218 | } 219 | } 220 | } 221 | } 222 | 223 | // Devices View 224 | struct DevicesView: View { 225 | @ObservedObject var viewModel: ABMViewModel 226 | @State private var selectedDevice: OrgDevice? 227 | @State private var showingDetails = false 228 | @State private var searchText = "" 229 | @State private var showingExporter = false 230 | 231 | var filteredDevices: [OrgDevice] { 232 | if searchText.isEmpty { 233 | return viewModel.devices 234 | } else { 235 | return viewModel.devices.filter { device in 236 | device.serialNumber.localizedCaseInsensitiveContains(searchText) || 237 | (device.name ?? "").localizedCaseInsensitiveContains(searchText) || 238 | (device.model ?? "").localizedCaseInsensitiveContains(searchText) 239 | } 240 | } 241 | } 242 | 243 | var body: some View { 244 | VStack(spacing: 0) { 245 | // Header 246 | HStack(spacing: 16) { 247 | Text("Devices") 248 | .font(.largeTitle) 249 | .fontWeight(.bold) 250 | 251 | Spacer() 252 | 253 | // Search bar 254 | HStack { 255 | Image(systemName: "magnifyingglass") 256 | .foregroundColor(.secondary) 257 | TextField("Search devices...", text: $searchText) 258 | .textFieldStyle(.plain) 259 | } 260 | .padding(8) 261 | .background(Color.gray.opacity(0.1)) 262 | .cornerRadius(8) 263 | .frame(maxWidth: 300) 264 | 265 | // Action buttons 266 | HStack(spacing: 12) { 267 | Button(action: { showingExporter = true }) { 268 | Label("Export", systemImage: "square.and.arrow.up") 269 | } 270 | .buttonStyle(.bordered) 271 | .disabled(viewModel.devices.isEmpty) 272 | 273 | Button(action: { viewModel.fetchDevices() }) { 274 | Label("Refresh", systemImage: "arrow.clockwise") 275 | } 276 | .buttonStyle(.borderedProminent) 277 | } 278 | } 279 | .padding() 280 | .background(Color(NSColor.windowBackgroundColor)) 281 | 282 | Divider() 283 | 284 | // Device list 285 | if viewModel.devices.isEmpty { 286 | Spacer() 287 | ContentUnavailableView( 288 | "No Devices", 289 | systemImage: "laptopcomputer", 290 | description: Text("Click 'Connect to ABM' in the sidebar to load devices") 291 | ) 292 | Spacer() 293 | } else if filteredDevices.isEmpty { 294 | Spacer() 295 | ContentUnavailableView.search(text: searchText) 296 | Spacer() 297 | } else { 298 | List(filteredDevices, selection: $selectedDevice) { device in 299 | DeviceRow(device: device) 300 | .onTapGesture { 301 | selectedDevice = device 302 | showingDetails = true 303 | } 304 | } 305 | .listStyle(.inset(alternatesRowBackgrounds: true)) 306 | 307 | // Status bar 308 | HStack { 309 | Text("\(filteredDevices.count) devices") 310 | .font(.caption) 311 | .foregroundColor(.secondary) 312 | Spacer() 313 | } 314 | .padding(.horizontal) 315 | .padding(.vertical, 8) 316 | .background(Color(NSColor.controlBackgroundColor)) 317 | } 318 | } 319 | .sheet(isPresented: $showingDetails) { 320 | if let device = selectedDevice { 321 | DeviceDetailView(device: device, viewModel: viewModel) 322 | } 323 | } 324 | .fileExporter( 325 | isPresented: $showingExporter, 326 | document: CSVDocument(devices: filteredDevices), 327 | contentType: .commaSeparatedText, 328 | defaultFilename: "devices_\(Date().formatted(date: .abbreviated, time: .omitted)).csv" 329 | ) { result in 330 | switch result { 331 | case .success(let url): 332 | viewModel.statusMessage = "Exported to \(url.lastPathComponent)" 333 | case .failure(let error): 334 | viewModel.errorMessage = "Export failed: \(error.localizedDescription)" 335 | } 336 | } 337 | } 338 | } 339 | 340 | // Device Row 341 | struct DeviceRow: View { 342 | let device: OrgDevice 343 | 344 | var deviceIcon: String { 345 | switch device.os?.lowercased() { 346 | case "ios": return "iphone" 347 | case "ipados": return "ipad" 348 | case "macos": return "desktopcomputer" 349 | case "tvos": return "appletv" 350 | default: return "questionmark.square" 351 | } 352 | } 353 | 354 | var body: some View { 355 | HStack(spacing: 12) { 356 | Image(systemName: deviceIcon) 357 | .font(.title2) 358 | .foregroundColor(.blue) 359 | .frame(width: 30) 360 | 361 | VStack(alignment: .leading, spacing: 4) { 362 | Text(device.name ?? device.serialNumber) 363 | .font(.system(.body, weight: .medium)) 364 | 365 | HStack(spacing: 8) { 366 | if let model = device.model { 367 | Label(model, systemImage: "info.circle") 368 | .font(.caption) 369 | .foregroundColor(.secondary) 370 | } 371 | 372 | if let os = device.os, let version = device.osVersion { 373 | Label("\(os) \(version)", systemImage: "gear") 374 | .font(.caption) 375 | .foregroundColor(.secondary) 376 | } 377 | } 378 | } 379 | 380 | Spacer() 381 | 382 | Text(device.enrollmentState ?? "Unknown") 383 | .font(.caption) 384 | .padding(.horizontal, 8) 385 | .padding(.vertical, 4) 386 | .background(device.enrollmentState == "ASSIGNED" ? Color.green.opacity(0.2) : Color.gray.opacity(0.2)) 387 | .cornerRadius(4) 388 | } 389 | .padding(.vertical, 4) 390 | .contentShape(Rectangle()) 391 | } 392 | } 393 | 394 | // Device Detail View 395 | struct DeviceDetailView: View { 396 | let device: OrgDevice 397 | @ObservedObject var viewModel: ABMViewModel 398 | @State private var deviceDetails: OrgDevice? 399 | @State private var assignedServer: String? 400 | @Environment(\.dismiss) var dismiss 401 | 402 | var body: some View { 403 | NavigationView { 404 | Form { 405 | Section("Device Information") { 406 | LabeledContent("Serial Number", value: device.serialNumber) 407 | LabeledContent("Name", value: device.name ?? "N/A") 408 | LabeledContent("Model", value: device.model ?? "N/A") 409 | LabeledContent("OS", value: "\(device.os ?? "N/A") \(device.osVersion ?? "")") 410 | LabeledContent("Status", value: device.enrollmentState ?? "N/A") 411 | LabeledContent("ID", value: device.id) 412 | } 413 | 414 | Section("Actions") { 415 | Button("Get Assigned Server") { 416 | Task { 417 | await viewModel.getDeviceAssignedServer(deviceId: device.id) 418 | } 419 | } 420 | 421 | if let server = assignedServer { 422 | LabeledContent("Assigned Server", value: server) 423 | } 424 | } 425 | } 426 | .navigationTitle("Device Details") 427 | .toolbar { 428 | ToolbarItem(placement: .confirmationAction) { 429 | Button("Done") { 430 | dismiss() 431 | } 432 | } 433 | } 434 | } 435 | .frame(minWidth: 500, minHeight: 400) 436 | } 437 | } 438 | 439 | // MDM Servers View 440 | struct MDMServersView: View { 441 | @ObservedObject var viewModel: ABMViewModel 442 | @State private var selectedServer: MDMServer? 443 | 444 | var body: some View { 445 | VStack(spacing: 0) { 446 | // Header 447 | VStack(spacing: 16) { 448 | HStack { 449 | Text("MDM Servers") 450 | .font(.largeTitle) 451 | .fontWeight(.bold) 452 | Spacer() 453 | } 454 | .padding(.horizontal) 455 | .padding(.top) 456 | 457 | Divider() 458 | } 459 | 460 | // Content 461 | if viewModel.mdmServers.isEmpty { 462 | ContentUnavailableView( 463 | "No MDM Servers", 464 | systemImage: "server.rack", 465 | description: Text("Click 'Connect to ABM' in the sidebar to load servers") 466 | ) 467 | } else { 468 | List(viewModel.mdmServers, selection: $selectedServer) { server in 469 | VStack(alignment: .leading) { 470 | Text(server.attributes.serverName) 471 | .font(.headline) 472 | Text(server.attributes.serverType) 473 | .font(.caption) 474 | .foregroundColor(.secondary) 475 | Text("ID: \(server.id)") 476 | .font(.caption2) 477 | .foregroundColor(.secondary) 478 | } 479 | .padding(.vertical, 4) 480 | } 481 | .listStyle(.inset(alternatesRowBackgrounds: true)) 482 | 483 | if let server = selectedServer { 484 | VStack(alignment: .leading, spacing: 12) { 485 | Divider() 486 | HStack { 487 | Text("Selected: \(server.attributes.serverName)") 488 | .font(.headline) 489 | Spacer() 490 | Button("Get Devices") { 491 | Task { 492 | await viewModel.getDevicesForMDM(mdmId: server.id) 493 | } 494 | } 495 | .buttonStyle(.borderedProminent) 496 | } 497 | .padding() 498 | } 499 | .background(Color(NSColor.controlBackgroundColor)) 500 | } 501 | } 502 | } 503 | } 504 | } 505 | 506 | // Device Assignment View 507 | struct DeviceAssignmentView: View { 508 | @ObservedObject var viewModel: ABMViewModel 509 | @State private var selectedDevices: Set = [] 510 | @State private var selectedMDM: String = "" 511 | @State private var actionType = "ASSIGN" 512 | 513 | var body: some View { 514 | VStack { 515 | Text("Device Assignment") 516 | .font(.largeTitle) 517 | .padding() 518 | 519 | Form { 520 | Section("Select Action") { 521 | Picker("Action", selection: $actionType) { 522 | Text("Assign").tag("ASSIGN") 523 | Text("Unassign").tag("UNASSIGN") 524 | } 525 | .pickerStyle(.segmented) 526 | } 527 | 528 | if actionType == "ASSIGN" { 529 | Section("Select MDM Server") { 530 | Picker("MDM Server", selection: $selectedMDM) { 531 | Text("Select Server").tag("") 532 | ForEach(viewModel.mdmServers) { server in 533 | Text(server.attributes.serverName).tag(server.id) 534 | } 535 | } 536 | } 537 | } 538 | 539 | Section("Select Devices") { 540 | List(viewModel.devices, selection: $selectedDevices) { device in 541 | HStack { 542 | Text(device.name ?? device.serialNumber) 543 | Spacer() 544 | if selectedDevices.contains(device.id) { 545 | Image(systemName: "checkmark.circle.fill") 546 | .foregroundColor(.blue) 547 | } 548 | } 549 | } 550 | .frame(minHeight: 200) 551 | } 552 | 553 | Button("Execute") { 554 | Task { 555 | await viewModel.assignDevices( 556 | deviceIds: Array(selectedDevices), 557 | mdmId: actionType == "ASSIGN" ? selectedMDM : nil 558 | ) 559 | } 560 | } 561 | .buttonStyle(.borderedProminent) 562 | .disabled(selectedDevices.isEmpty || (actionType == "ASSIGN" && selectedMDM.isEmpty)) 563 | } 564 | } 565 | } 566 | } 567 | 568 | // Activity Status View 569 | struct ActivityStatusView: View { 570 | @ObservedObject var viewModel: ABMViewModel 571 | @State private var activityId = "" 572 | 573 | var body: some View { 574 | VStack(spacing: 0) { 575 | // Header 576 | VStack(spacing: 16) { 577 | HStack { 578 | Text("Activity Status") 579 | .font(.largeTitle) 580 | .fontWeight(.bold) 581 | Spacer() 582 | } 583 | .padding(.horizontal) 584 | .padding(.top) 585 | 586 | Divider() 587 | } 588 | 589 | // Content 590 | VStack(spacing: 24) { 591 | Form { 592 | Section("Check Activity Status") { 593 | HStack { 594 | TextField("Activity ID", text: $activityId) 595 | .textFieldStyle(.roundedBorder) 596 | Button("Check Status") { 597 | Task { 598 | await viewModel.checkActivityStatus(activityId: activityId) 599 | } 600 | } 601 | .buttonStyle(.borderedProminent) 602 | .disabled(activityId.isEmpty) 603 | } 604 | } 605 | 606 | if let status = viewModel.activityStatus { 607 | Section("Status") { 608 | HStack { 609 | Text("ID") 610 | .fontWeight(.medium) 611 | .frame(width: 100, alignment: .leading) 612 | Text(status.data.id) 613 | .textSelection(.enabled) 614 | .font(.system(.body, design: .monospaced)) 615 | Spacer() 616 | Button(action: { 617 | NSPasteboard.general.clearContents() 618 | NSPasteboard.general.setString(status.data.id, forType: .string) 619 | }) { 620 | Image(systemName: "doc.on.doc") 621 | .foregroundColor(.blue) 622 | } 623 | .buttonStyle(.plain) 624 | .help("Copy Activity ID") 625 | } 626 | 627 | LabeledContent("Status", value: status.data.attributes.status) 628 | LabeledContent("Sub-Status", value: status.data.attributes.subStatus) 629 | LabeledContent("Created", value: status.data.attributes.createdDateTime) 630 | } 631 | } 632 | 633 | if let lastId = viewModel.lastActivityId { 634 | Section("Recent Activity") { 635 | HStack { 636 | Text("Last Activity ID") 637 | .fontWeight(.medium) 638 | Spacer() 639 | Text(lastId) 640 | .textSelection(.enabled) 641 | .font(.system(.body, design: .monospaced)) 642 | .lineLimit(1) 643 | Button(action: { 644 | activityId = lastId 645 | Task { 646 | await viewModel.checkActivityStatus(activityId: lastId) 647 | } 648 | }) { 649 | Image(systemName: "arrow.clockwise") 650 | .foregroundColor(.blue) 651 | } 652 | .buttonStyle(.plain) 653 | .help("Check this activity") 654 | } 655 | } 656 | } 657 | } 658 | .formStyle(.grouped) 659 | .frame(maxWidth: 800) 660 | 661 | Spacer() 662 | } 663 | .padding() 664 | } 665 | } 666 | } 667 | 668 | #Preview { 669 | ContentView() 670 | } 671 | --------------------------------------------------------------------------------