# MDM Device Link für Magic App — Design ## Ziel In der ReBreak Magic App unter „Meine iOS-Geräte“ automatisch prüfen, ob ein iOS-Gerät im NanoMDM enrolled ist, und den passenden Status in der Device-Card anzeigen. ## Annahmen / Einschränkungen - Native-App-DB (`rebreak` auf `rebreak-server`) und NanoMDM-DB (`nanomdm` auf `rebreak-mdm`) liegen auf **unterschiedlichen Hetzner-VPS**. - Apple gibt die Hardware-UDID **nicht** an React Native / Expo Apps weiter. Die Native-App-`deviceId` ist `identifierForVendor`, nicht die NanoMDM-UDID. - Daher braucht `UserDevice` eine zusätzliche Spalte `mdmId`, die die NanoMDM-UDID speichert. ## Architektur ``` Magic App (Tauri/Nuxt) │ ├─ GET /api/magic/devices/:deviceId/mdm → Backend │ │ │ ├─ Prisma: UserDevice.mdmId lesen │ │ │ └─ pg.Pool → NanoMDM DB auf rebreak-mdm │ SELECT devices / command_results │ └─ POST /api/magic/devices/:deviceId/mdm-link (setzt UserDevice.mdmId, z.B. nach USB-Enrollment) ``` ## Datenmodell ```prisma model UserDevice { // ... bestehende Felder ... mdmId String? @map("mdm_id") // NanoMDM-UDID, nullable } model DeviceProtectionState { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid deviceId String @map("device_id") platform String protectionType String @map("protection_type") active Boolean lastSeenAt DateTime? @map("last_seen_at") changedAt DateTime @default(now()) @map("changed_at") reason String? @@unique([userId, deviceId, protectionType]) @@map("device_protection_states") @@schema("rebreak") } model DeviceProtectionStateLog { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid deviceId String @map("device_id") protectionType String @map("protection_type") active Boolean occurredAt DateTime @map("occurred_at") reason String? source String @@index([userId, deviceId]) @@map("device_protection_state_logs") @@schema("rebreak") } ``` ## Endpunkte ### `GET /api/magic/devices/:deviceId/mdm` Auth: Magic-Session (`requireUser`) Response enrolled: ```json { "success": true, "data": { "enrolled": true, "company": "ReBreak", "supervised": true, "lockProfileInstalled": true, "lastAppPushAt": "2026-06-11T19:09:04.363Z" } } ``` Response not enrolled: ```json { "success": true, "data": { "enrolled": false } } ``` Wenn `mdmId` gesetzt ist, aber NanoMDM das Gerät nicht mehr kennt, wird `mdmId` automatisch auf `null` gesetzt. `lockProfileInstalled` wird nicht aus der bloßen MDM-Enrollment abgeleitet, sondern aus dem lokal gespeicherten `DeviceProtectionState` mit `protectionType = "nefilter"`. Solange die Native-App diesen Zustand noch nicht meldet, ist der Wert `false`. ### `POST /api/magic/devices/:deviceId/mdm-link` Body: `{ "mdmId": "00008150-001C686601F0401C" }` Setzt `UserDevice.mdmId` für das iOS-Gerät des aktuellen Users. ### `POST /api/devices/protection-state` Body: ```json { "deviceId": "string", "platform": "ios", "protectionType": "nefilter | vpn | dns", "active": true, "reason": "optional", "source": "optional" } ``` Upsertet den per-Gerät-Schutz-Status und schreibt bei Änderung einen Log-Eintrag. ## Magic-App UI `IosDeviceCard.vue` ruft über `useMdmStatus(deviceId)` den Backend-Status ab: - **Enrolled**: Grüne Box mit Company / Supervised / Lock-Profil / Letzter App-Push. - **Not enrolled**: Gelbe Box mit Hinweis: „Nicht MDM-enrolled. Verbinde das iPhone per USB. Das Enrollment dauert ca. 2 Minuten und geht ohne Datenverlust.“ - Wenn ein iPhone per USB verbunden ist, erscheint ein Button „Mit MDM verknüpfen“, der `linkMdmDevice(deviceId, udid)` aufruft. ## Infra / Env - `MDM_DATABASE_URL` muss im Backend-Env gesetzt sein. - `rebreak-mdm` PostgreSQL muss auf der öffentlichen IP lauschen und den Backend-Server in `pg_hba.conf` + UFW erlauben. ## Dateien Backend: - `backend/prisma/schema.prisma` - `backend/nitro.config.ts` - `backend/server/db/mdm.ts` - `backend/server/db/device-protection.ts` - `backend/server/api/magic/devices/[deviceId]/mdm.get.ts` - `backend/server/api/magic/devices/[deviceId]/mdm-link.post.ts` - `backend/server/api/devices/protection-state.post.ts` - `backend/start-staging.sh` Magic App: - `apps/rebreak-magic/src-tauri/src/backend/api.rs` - `apps/rebreak-magic/src-tauri/src/lib.rs` - `apps/rebreak-magic/app/composables/useTauri.ts` - `apps/rebreak-magic/app/composables/useMdmStatus.ts` - `apps/rebreak-magic/app/components/IosDeviceCard.vue` - `apps/rebreak-magic/app/components/DevLogDrawer.vue` ## Test-Ergebnis (Staging) - Backend-Build: ✅ - Magic Rust: `cargo check` ✅ - Magic Nuxt-Build: ✅ - API-Test für User `charioanouar@gmail.com`, iPhone `MHFLX23QM0`: - `GET .../mdm` → `{ enrolled: true, company: "ReBreak", supervised: true, lockProfileInstalled: false, lastAppPushAt: "2026-06-11T19:09:04.363Z" }` - `mdmId` gelöscht → `{ enrolled: false }` - `POST .../mdm-link` → `{ mdmId: "00008150-001C686601F0401C" }` ## Offene TODOs - `nefilter`, `vpn`, `dns` werden noch nicht von den Clients gemeldet. Dafür ist `POST /api/devices/protection-state` vorbereitet. - Die Tabellen `device_protection_states` / `device_protection_state_logs` wurden für den schnellen Test manuell angelegt. Für Produktion muss eine Prisma-Migration erstellt und deployed werden. - Die Spalte `mdm_id` wurde für den schnellen Test manuell mit `ALTER TABLE` angelegt.