rebreak-monorepo/docs/superpowers/specs/2026-06-17-mdm-device-link-design.md

163 lines
5.8 KiB
Markdown

# 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.