163 lines
5.8 KiB
Markdown
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.
|