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 ────────────────────────────────────────────────────
|
// ─── Admin / Cron ────────────────────────────────────────────────────
|
||||||
adminSecret: process.env.ADMIN_SECRET ?? "",
|
adminSecret: process.env.ADMIN_SECRET ?? "",
|
||||||
cronSecret: process.env.CRON_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).
|
// Shared secret for AdGuard→Backend DoH-handshake (POST /api/devices/protected/handshake).
|
||||||
// Set in Infisical as HANDSHAKE_SECRET before enabling AdGuard webhook.
|
// Set in Infisical as HANDSHAKE_SECRET before enabling AdGuard webhook.
|
||||||
handshakeSecret: process.env.HANDSHAKE_SECRET ?? "",
|
handshakeSecret: process.env.HANDSHAKE_SECRET ?? "",
|
||||||
|
|||||||
@ -4,8 +4,10 @@ import {
|
|||||||
appendProtectionEventDeduped,
|
appendProtectionEventDeduped,
|
||||||
type ProtectionSource,
|
type ProtectionSource,
|
||||||
} from "../../db/protectionStateLog";
|
} from "../../db/protectionStateLog";
|
||||||
import { upsertDeviceProtectionState } from "../../db/device-protection";
|
import {
|
||||||
import type { ProtectionType } from "../../db/device-protection";
|
upsertDeviceProtectionState,
|
||||||
|
type ProtectionType,
|
||||||
|
} from "../../db/device-protection";
|
||||||
|
|
||||||
const VALID_SOURCES: ProtectionSource[] = ["vpn", "mdm", "client"];
|
const VALID_SOURCES: ProtectionSource[] = ["vpn", "mdm", "client"];
|
||||||
|
|
||||||
@ -14,6 +16,17 @@ function sourceToProtectionType(source: ProtectionSource): ProtectionType {
|
|||||||
return "vpn";
|
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
|
* POST /api/protection/event
|
||||||
*
|
*
|
||||||
@ -23,6 +36,9 @@ function sourceToProtectionType(source: ProtectionSource): ProtectionType {
|
|||||||
* deviceId?: string // optional, falls bekannt (z.B. native App)
|
* 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
|
* Called from the native app (useProtectionState / lib/protection) when the
|
||||||
* combined protection state transitions on↔off. The client deduplicates
|
* combined protection state transitions on↔off. The client deduplicates
|
||||||
* locally (only fires on real transitions); the server deduplicates again
|
* 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).
|
* { success: true, written: false } if deduplicated (state unchanged).
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const user = await requireUser(event);
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
if (typeof body?.active !== "boolean") {
|
if (typeof body?.active !== "boolean") {
|
||||||
@ -47,13 +62,42 @@ export default defineEventHandler(async (event) => {
|
|||||||
? (body.source as ProtectionSource)
|
? (body.source as ProtectionSource)
|
||||||
: "client";
|
: "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;
|
const deviceId = body?.deviceId ?? getHeader(event, "x-device-id") ?? null;
|
||||||
if (deviceId && typeof deviceId === "string") {
|
if (deviceId && typeof deviceId === "string") {
|
||||||
try {
|
try {
|
||||||
await upsertDeviceProtectionState(
|
await upsertDeviceProtectionState(
|
||||||
user.id,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
"ios",
|
"ios",
|
||||||
sourceToProtectionType(source),
|
sourceToProtectionType(source),
|
||||||
@ -63,7 +107,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
"native-app",
|
"native-app",
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} 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