From 97f8d593a5bd0aae02da828623ac39bcd8d50128 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 09:10:33 +0200 Subject: [PATCH] feat(protection): /api/protection/event aktualisiert device_protection_states - Backend: /api/protection/event setzt bei Vorhandensein von deviceId (Body oder x-device-id Header) auch device_protection_states. source=mdm -> protectionType=nefilter, sonst vpn. - Native App: sendet deviceId im Body von /api/protection/event. - Magic App: Lock-Profil-Status wird nach lokaler Installation ans Backend gemeldet und Backend-Status neu geladen. --- .../hooks/useProtectionState.ts | 12 +++++-- backend/server/api/protection/event.post.ts | 36 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/rebreak-native/hooks/useProtectionState.ts b/apps/rebreak-native/hooks/useProtectionState.ts index 9979ba8..ad1d4d2 100644 --- a/apps/rebreak-native/hooks/useProtectionState.ts +++ b/apps/rebreak-native/hooks/useProtectionState.ts @@ -8,6 +8,7 @@ import { formatCooldownRemaining, } from '../lib/protection'; import { apiFetch } from '../lib/api'; +import { getDeviceId } from '../lib/deviceId'; import type { WebContentFilterResult } from '../modules/rebreak-protection'; const POLL_MS_ACTIVE_COOLDOWN = 5_000; @@ -210,7 +211,7 @@ export function useProtectionState(): UseProtectionStateReturn { return () => sub?.remove(); }, [fetchState]); - // Report protection-state transitions to the coverage log. + // Report protection-state transitions to the coverage log and per-device state. // Fires only on genuine active↔inactive flips; deduped via ref. useEffect(() => { if (state === null) return; @@ -218,7 +219,14 @@ export function useProtectionState(): UseProtectionStateReturn { if (lastReportedActiveRef.current === active) return; lastReportedActiveRef.current = active; const source = resolveEventSource(state); - apiFetch('/api/protection/event', { method: 'POST', body: { active, source } }).catch(() => {}); + getDeviceId() + .then((deviceId) => { + apiFetch('/api/protection/event', { + method: 'POST', + body: { active, source, deviceId }, + }).catch(() => {}); + }) + .catch(() => {}); }, [state]); // ─── Public Actions ──────────────────────────────────────────────── diff --git a/backend/server/api/protection/event.post.ts b/backend/server/api/protection/event.post.ts index 5f14a01..48f2a67 100644 --- a/backend/server/api/protection/event.post.ts +++ b/backend/server/api/protection/event.post.ts @@ -1,21 +1,37 @@ +import { getHeader } from "h3"; import { requireUser } from "../../utils/auth"; import { appendProtectionEventDeduped, type ProtectionSource, } from "../../db/protectionStateLog"; +import { upsertDeviceProtectionState } from "../../db/device-protection"; +import type { ProtectionType } from "../../db/device-protection"; const VALID_SOURCES: ProtectionSource[] = ["vpn", "mdm", "client"]; +function sourceToProtectionType(source: ProtectionSource): ProtectionType { + if (source === "mdm") return "nefilter"; + return "vpn"; +} + /** * POST /api/protection/event * - * Body: { active: boolean, source: 'vpn' | 'mdm' | 'client' } + * Body: { + * active: boolean, + * source: 'vpn' | 'mdm' | 'client', + * deviceId?: string // optional, falls bekannt (z.B. native App) + * } * * Called from the native app (useProtectionState / lib/protection) when the * combined protection state transitions on↔off. The client deduplicates * locally (only fires on real transitions); the server deduplicates again * against the last DB row for the user. * + * Side-effect: if deviceId is provided (body or x-device-id header), the + * per-device protection state (device_protection_states) is also updated so + * that MDM / Magic-App views see the current nefilter/vpn status. + * * Returns { success: true, written: true } if a new row was written, * { success: true, written: false } if deduplicated (state unchanged). */ @@ -33,5 +49,23 @@ export default defineEventHandler(async (event) => { const row = await appendProtectionEventDeduped(user.id, body.active, source); + const deviceId = body?.deviceId ?? getHeader(event, "x-device-id") ?? null; + if (deviceId && typeof deviceId === "string") { + try { + await upsertDeviceProtectionState( + user.id, + deviceId, + "ios", + sourceToProtectionType(source), + body.active, + new Date(), + `protection event via ${source}`, + "native-app", + ); + } catch (err: any) { + console.error("[protection/event] device_protection_states upsert failed:", err?.message ?? err); + } + } + return { success: true, written: row !== null }; });