feat(backend): /api/protection/event akzeptiert Extension-Secret
- Extension-Auth-Path via x-extension-secret Header. - Ermittelt userId anhand deviceId aus UserDevice. - EXTENSION_SECRET in runtimeConfig + Infisical staging.
This commit is contained in:
parent
a7ac5545ae
commit
b0a7091ac7
@ -71,6 +71,9 @@ export default defineNitroConfig({
|
||||
// ─── Admin / Cron ────────────────────────────────────────────────────
|
||||
adminSecret: process.env.ADMIN_SECRET ?? "",
|
||||
cronSecret: process.env.CRON_SECRET ?? "",
|
||||
// Shared secret for iOS Network Extensions to report protection-state
|
||||
// transitions when the container app is not running.
|
||||
extensionSecret: process.env.EXTENSION_SECRET ?? "",
|
||||
// Shared secret for AdGuard→Backend DoH-handshake (POST /api/devices/protected/handshake).
|
||||
// Set in Infisical as HANDSHAKE_SECRET before enabling AdGuard webhook.
|
||||
handshakeSecret: process.env.HANDSHAKE_SECRET ?? "",
|
||||
|
||||
@ -4,8 +4,10 @@ import {
|
||||
appendProtectionEventDeduped,
|
||||
type ProtectionSource,
|
||||
} from "../../db/protectionStateLog";
|
||||
import { upsertDeviceProtectionState } from "../../db/device-protection";
|
||||
import type { ProtectionType } from "../../db/device-protection";
|
||||
import {
|
||||
upsertDeviceProtectionState,
|
||||
type ProtectionType,
|
||||
} from "../../db/device-protection";
|
||||
|
||||
const VALID_SOURCES: ProtectionSource[] = ["vpn", "mdm", "client"];
|
||||
|
||||
@ -14,6 +16,17 @@ function sourceToProtectionType(source: ProtectionSource): ProtectionType {
|
||||
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
|
||||
*
|
||||
@ -23,6 +36,9 @@ function sourceToProtectionType(source: ProtectionSource): ProtectionType {
|
||||
* 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
|
||||
@ -36,7 +52,6 @@ function sourceToProtectionType(source: ProtectionSource): ProtectionType {
|
||||
* { 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") {
|
||||
@ -47,13 +62,42 @@ export default defineEventHandler(async (event) => {
|
||||
? (body.source as ProtectionSource)
|
||||
: "client";
|
||||
|
||||
const row = await appendProtectionEventDeduped(user.id, body.active, source);
|
||||
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(
|
||||
user.id,
|
||||
userId,
|
||||
deviceId,
|
||||
"ios",
|
||||
sourceToProtectionType(source),
|
||||
@ -63,7 +107,10 @@ export default defineEventHandler(async (event) => {
|
||||
"native-app",
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error("[protection/event] device_protection_states upsert failed:", err?.message ?? err);
|
||||
console.error(
|
||||
"[protection/event] device_protection_states upsert failed:",
|
||||
err?.message ?? err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user