- Extension-Auth-Path via x-extension-secret Header. - Ermittelt userId anhand deviceId aus UserDevice. - EXTENSION_SECRET in runtimeConfig + Infisical staging.
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { getHeader } from "h3";
|
|
import { requireUser } from "../../utils/auth";
|
|
import {
|
|
appendProtectionEventDeduped,
|
|
type ProtectionSource,
|
|
} from "../../db/protectionStateLog";
|
|
import {
|
|
upsertDeviceProtectionState,
|
|
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";
|
|
}
|
|
|
|
async function resolveUserIdFromDeviceId(
|
|
deviceId: string,
|
|
): Promise<string | null> {
|
|
const db = usePrisma();
|
|
const row = await db.userDevice.findUnique({
|
|
where: { deviceId },
|
|
select: { userId: true },
|
|
});
|
|
return row?.userId ?? null;
|
|
}
|
|
|
|
/**
|
|
* POST /api/protection/event
|
|
*
|
|
* Body: {
|
|
* active: boolean,
|
|
* source: 'vpn' | 'mdm' | 'client',
|
|
* deviceId?: string // optional, falls bekannt (z.B. native App)
|
|
* }
|
|
*
|
|
* Auth: Bearer <jwt> (native app) OR x-extension-secret: <secret> (iOS
|
|
* Network-Extension when the container app is not running).
|
|
*
|
|
* 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 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";
|
|
|
|
let userId: string;
|
|
|
|
// ─── Extension auth path (iOS Network Extension) ────────────────────────────
|
|
const extensionSecret = getHeader(event, "x-extension-secret");
|
|
const config = useRuntimeConfig(event);
|
|
const expectedSecret = (config as any).extensionSecret as string | undefined;
|
|
|
|
if (extensionSecret && extensionSecret === expectedSecret && expectedSecret) {
|
|
const deviceId = body?.deviceId;
|
|
if (!deviceId || typeof deviceId !== "string") {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: "deviceId required for extension auth",
|
|
});
|
|
}
|
|
const resolved = await resolveUserIdFromDeviceId(deviceId);
|
|
if (!resolved) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
message: "device not found",
|
|
});
|
|
}
|
|
userId = resolved;
|
|
} else {
|
|
// ─── Standard auth path (container app with JWT) ──────────────────────────
|
|
const user = await requireUser(event);
|
|
userId = user.id;
|
|
}
|
|
|
|
const row = await appendProtectionEventDeduped(userId, body.active, source);
|
|
|
|
const deviceId = body?.deviceId ?? getHeader(event, "x-device-id") ?? null;
|
|
if (deviceId && typeof deviceId === "string") {
|
|
try {
|
|
await upsertDeviceProtectionState(
|
|
userId,
|
|
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 };
|
|
});
|