chahinebrini 87d6395ed2 fix(magic-mac): macOS 26 profile install via NSWorkspace + de-dup register card
Zwei Bugs:

1) 'profiles install -path' wurde mit macOS 15+ entfernt
   ('profiles tool no longer supports installs. Use System Settings
   Profiles to add configuration profiles.'). Auf macOS 26 (Tahoe)
   ist das Hard-Removal.
   -> Switch zu NSWorkspace.shared.open(profileURL): \u00f6ffnet die
   .mobileconfig in System Settings -> Profile-Pane. User best\u00e4tigt
   manuell + gibt Admin-PW. Einziger Weg ohne MDM-Enrollment.
   -> success-Text passt: 'Bitte in System Settings Installieren
   klicken'.

2) Doppelte 'Mac registriert'-Karte: successMessage-Card UND
   strukturierte Registration-Status-Card beide sichtbar nach
   register. Auto-Profile-Install nach Register war eh totes
   Verhalten (DNS jetzt optional).
   -> successMessage wird nicht mehr in handleRegistration gesetzt,
   nur noch in handleProfileInstall. Eine Karte.
2026-06-03 10:29:30 +02:00

107 lines
4.1 KiB
Swift

import Foundation
import AppKit
/// Service für Mac-DNS-Profile-Download + Installation.
enum MacProfileInstaller {
enum InstallerError: Error, LocalizedError {
case noRegistration
case downloadFailed(String)
case installFailed(String)
var errorDescription: String? {
switch self {
case .noRegistration:
return "Mac ist nicht registriert. Bitte zuerst registrieren."
case .downloadFailed(let msg):
return "Profile-Download fehlgeschlagen: \(msg)"
case .installFailed(let msg):
return "Profile-Installation fehlgeschlagen: \(msg)"
}
}
}
/// Lädt Mac-DNS-Profile von Backend und öffnet es in System Settings Profiles.
/// Ab macOS 15+ ist `profiles install` für Configuration Profiles entfernt
/// ("profiles tool no longer supports installs. Use System Settings
/// Profiles to add configuration profiles."). Einzig zulässiger Weg ohne
/// MDM-Enrollment: NSWorkspace öffnet die .mobileconfig Profiles-Pane
/// erscheint User muss manuell Installieren" klicken + Admin-PW geben.
static func downloadAndInstall(registration: MagicRegistration) async throws {
// 1. Download profile
let profileURL: URL
do {
profileURL = try await MagicAPIClient.shared.downloadProfile(token: registration.dnsToken)
} catch {
throw InstallerError.downloadFailed(error.localizedDescription)
}
// 2. Open in System Settings Profiles (user must confirm in UI)
let opened = await MainActor.run {
NSWorkspace.shared.open(profileURL)
}
if !opened {
try? FileManager.default.removeItem(at: profileURL)
throw InstallerError.installFailed(
"System Settings konnte das Profil nicht öffnen. Datei liegt unter: \(profileURL.path)"
)
}
// NICHT löschen System Settings braucht die Datei evtl. noch.
// OS räumt /tmp selbst auf.
}
/// Prüft ob ReBreak-DNS-Profile bereits installiert ist.
/// Verwendet `profiles show -type configuration`.
static func isInstalled() async -> Bool {
guard let result = try? await ProcessRunner.run(
"/usr/bin/profiles",
arguments: ["show", "-type", "configuration"]
), result.exitCode == 0 else {
return false
}
// Suche nach PayloadIdentifier pattern org.rebreak.protection.dns.filter*
return result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.dns.filter")
|| result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.profile")
}
/// Entfernt ReBreak-DNS-Profile (für Testing/Reset).
/// Benötigt PayloadIdentifier wir suchen nach "org.rebreak.protection.profile.*".
static func remove() async throws {
// 1. Find identifier
guard let result = try? await ProcessRunner.run(
"/usr/bin/profiles",
arguments: ["show", "-type", "configuration"]
), result.exitCode == 0 else {
return
}
// Parse identifier aus Output (format: " <identifier>: <displayName>")
let lines = result.stdout.split(separator: "\n")
var identifier: String?
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("org.rebreak.protection.profile") {
// Format: "org.rebreak.protection.profile.abc123: ReBreak Protection"
identifier = trimmed.split(separator: ":").first.map(String.init)
break
}
}
guard let id = identifier else { return }
// 2. Remove profile
let removeResult = try await ProcessRunner.run(
"/usr/bin/profiles",
arguments: ["remove", "-identifier", id]
)
if removeResult.exitCode != 0 {
throw InstallerError.installFailed(removeResult.stderr)
}
}
}