├── 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 | [](https://opensource.org/licenses/Apache-2.0)
4 | [](https://www.apple.com/macos/)
5 | [](https://developer.apple.com/swift/)
6 | [](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 |
--------------------------------------------------------------------------------