chahinebrini 97f8d593a5
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Deploy Staging / Build backend (Nitro) (push) Has been cancelled
Deploy Staging / Deploy zu Hetzner (push) Has been cancelled
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.
2026-06-18 09:10:33 +02:00

72 lines
2.4 KiB
TypeScript

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',
* 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).
*/
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const body = await readBody(event);
if (typeof body?.active !== "boolean") {
throw createError({ statusCode: 400, message: "active (boolean) required" });
}
const source: ProtectionSource = VALID_SOURCES.includes(body.source)
? (body.source as ProtectionSource)
: "client";
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 };
});