chahinebrini c1edef8abd feat(magic): RebreakMagic device-binding + DNS profile
- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
2026-06-02 09:15:19 +02:00

213 lines
6.9 KiB
Swift

import Foundation
import Observation
enum DebugSupervisionMode: String, CaseIterable, Identifiable {
case none
case forceSupervised
case forceUnsupervised
var id: String { rawValue }
var title: String {
switch self {
case .none: return "Aus"
case .forceSupervised: return "Force Supervised"
case .forceUnsupervised: return "Force Unsupervised"
}
}
}
@MainActor
@Observable
final class WizardModel {
var step: WizardStep = .macRegistration
var device: DeviceState?
var supervisionLog: [String] = []
var supervisionRunning: Bool = false
var supervisionError: String?
var enrollmentLog: [String] = []
var enrollmentRunning: Bool = false
var enrollmentError: String?
var configureLog: [String] = []
var configureRunning: Bool = false
var configureError: String?
var showAdvancedLogs: Bool = false
var cooldownEndsAt: Date?
// Debug-Reset State
var supervisionMode: DebugSupervisionMode = .none
var resetRunning: Bool = false
var resetStatus: String?
var resetAll: Bool = true
var resetEnrollmentProfile: Bool = true
var resetLockProfile: Bool = true
var resetApp: Bool = true
// Auth + Magic State
var authSession: AuthSession?
var showingLogin: Bool = false
var showingManageBindings: Bool = false
var magicRegistration: MagicRegistration?
var registrationError: String?
init() {
// Load existing session from keychain
authSession = AuthService.shared.currentSession()
showingLogin = (authSession == nil)
}
func advance() {
if let next = WizardStep(rawValue: step.rawValue + 1) {
step = next
}
}
func goTo(_ s: WizardStep) {
step = s
}
// MARK: - Mac Registration
/// Registriert den aktuellen Mac im Backend.
/// Wirft MagicError.limitReached falls Device-Limit erreicht.
func registerMac() async throws {
registrationError = nil
do {
let macInfo = try MacDeviceDetector.detect()
let registration = try await MagicAPIClient.shared.register(
deviceId: macInfo.deviceId,
hostname: macInfo.hostname,
model: macInfo.model,
osVersion: macInfo.osVersion
)
magicRegistration = registration
} catch let error as MagicError {
// Bei limit_reached öffne ManageBindingsView
if case .limitReached(_) = error {
showingManageBindings = true
}
registrationError = error.localizedDescription
throw error
} catch {
registrationError = error.localizedDescription
throw error
}
}
func handleLogin(session: AuthSession) {
authSession = session
showingLogin = false
}
func handleLogout() async {
await AuthService.shared.signOut()
authSession = nil
showingLogin = true
reset()
}
func reset() {
step = .macRegistration
device = nil
supervisionLog = []
enrollmentLog = []
configureLog = []
supervisionError = nil
enrollmentError = nil
configureError = nil
showAdvancedLogs = false
cooldownEndsAt = nil
resetStatus = nil
magicRegistration = nil
registrationError = nil
}
func startDebugReset() {
guard device != nil else {
resetStatus = "Kein iPhone erkannt."
return
}
resetRunning = true
resetStatus = "Führe Debug-Reset aus …"
Task {
do {
var changes: [String] = []
let removeEnrollment = resetAll || resetEnrollmentProfile
let removeLock = resetAll || resetLockProfile
let removeApp = resetAll || resetApp
let installedProfileIDs = await DeviceDetector.installedProfileIDs()
var profileIDs: [String] = []
if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) {
profileIDs.append(DeviceState.enrollmentProfileID)
}
if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) {
profileIDs.append(DeviceState.lockProfileID)
}
if !profileIDs.isEmpty {
try await DeviceDetector.removeProfiles(identifiers: profileIDs)
changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))")
}
if removeApp {
try await DeviceDetector.removeApp(bundleID: "org.rebreak.app")
changes.append("App gelöscht: org.rebreak.app")
}
switch supervisionMode {
case .forceSupervised:
_ = try await SuperviseRunner.supervise(verbose: false) { _ in }
changes.append("Mode gesetzt: supervised")
case .forceUnsupervised:
_ = try await SuperviseRunner.unsupervise { _ in }
changes.append("Mode gesetzt: unsupervised")
case .none:
break
}
let nowInstalledProfiles = await DeviceDetector.installedProfileIDs()
let nowApps = await DeviceDetector.installedAppBundleIDs()
let status = await DeviceDetector.readSupervisionStatus()
await MainActor.run {
if changes.isEmpty {
resetStatus = "Keine Aktion gewählt."
} else {
resetStatus = "\(changes.joined(separator: " · "))"
}
if var device = self.device {
device.installedProfileIDs = nowInstalledProfiles
device.installedAppBundleIDs = nowApps
device.isSupervised = status.isSupervised
device.supervisorOrgName = status.organizationName
device.isFmiOn = status.findMyEnabled
device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID)
if !nowApps.contains("org.rebreak.app") { device.isManaged = false }
if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false }
self.device = device
}
resetRunning = false
}
} catch {
await MainActor.run {
resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)"
resetRunning = false
}
}
}
}
}