feat(devices): protected device enrollment + mobileconfig generator
Backend: - ProtectedDevice prisma model + migration add_protected_devices - DB helpers: list/count/get/create/confirm/revoke - mobileconfig.ts utility — XML-escape, unique UUIDs per request - 5 endpoints under /api/devices/* (avoid /api/devices conflict with existing Capacitor UserDevice route by using /api/devices/protected for list) Phase 1: backend ready. DoH-server token-routing comes in phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3088526fc1
commit
677b67902b
@ -0,0 +1,23 @@
|
|||||||
|
-- Migration: add_protected_devices
|
||||||
|
-- Multi-Device DNS-Schutz für Legend-User.
|
||||||
|
-- Legend: bis zu 3 Geräte mit individuellem DoH-Token geschützt.
|
||||||
|
|
||||||
|
CREATE TABLE "rebreak"."protected_devices" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" UUID NOT NULL,
|
||||||
|
"dns_token" TEXT NOT NULL,
|
||||||
|
"platform" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"installed_at" TIMESTAMPTZ,
|
||||||
|
"last_dns_query_at" TIMESTAMPTZ,
|
||||||
|
"revoked_at" TIMESTAMPTZ,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT "protected_devices_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "protected_devices_dns_token_key" ON "rebreak"."protected_devices"("dns_token");
|
||||||
|
CREATE INDEX "protected_devices_user_id_idx" ON "rebreak"."protected_devices"("user_id");
|
||||||
|
CREATE INDEX "protected_devices_dns_token_idx" ON "rebreak"."protected_devices"("dns_token");
|
||||||
@ -709,3 +709,32 @@ model UserDevice {
|
|||||||
@@map("user_devices")
|
@@map("user_devices")
|
||||||
@@schema("rebreak")
|
@@schema("rebreak")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-Device DNS-Schutz: Legend-User können bis zu 3 Geräte (Mac/iOS/Android/Windows)
|
||||||
|
// mit einem per-Device DoH-Token schützen. Token wird in die mobileconfig/DoH-URL
|
||||||
|
// eingebettet — ist die einzige Auth für den DoH-Server (Phase 2).
|
||||||
|
model ProtectedDevice {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String @map("user_id") @db.Uuid
|
||||||
|
/// Per-Device DoH-Token, embedded in mobileconfig ServerURL.
|
||||||
|
/// Format: 32-char cryptographic-random hex.
|
||||||
|
dnsToken String @unique @map("dns_token")
|
||||||
|
/// "mac" | "windows" | "ios" | "android"
|
||||||
|
platform String
|
||||||
|
/// User-friendly label, z.B. "MacBook Pro" oder "Olfas iPhone"
|
||||||
|
label String
|
||||||
|
/// pending (enrolled, profile not installed yet) | active (user confirmed install) | revoked
|
||||||
|
status String @default("pending")
|
||||||
|
/// User confirmed install via App (not server-side verified yet — DoH-routing kommt in Phase 2)
|
||||||
|
installedAt DateTime? @map("installed_at")
|
||||||
|
/// Optional: DoH-server pingt das später (Phase 2, separater Sprint)
|
||||||
|
lastDnsQueryAt DateTime? @map("last_dns_query_at")
|
||||||
|
revokedAt DateTime? @map("revoked_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([dnsToken])
|
||||||
|
@@map("protected_devices")
|
||||||
|
@@schema("rebreak")
|
||||||
|
}
|
||||||
|
|||||||
26
backend/server/api/devices/[id]/confirm-installed.post.ts
Normal file
26
backend/server/api/devices/[id]/confirm-installed.post.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { confirmProtectedDeviceInstalled } from "../../../db/protectedDevices";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/devices/:id/confirm-installed
|
||||||
|
*
|
||||||
|
* User klickt "Installiert" in der App nach dem Profil-Download.
|
||||||
|
* Setzt installedAt = NOW() und status = "active".
|
||||||
|
*
|
||||||
|
* Auth: requireUser + ownership (confirmProtectedDeviceInstalled prüft userId).
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event);
|
||||||
|
const id = getRouterParam(event, "id");
|
||||||
|
if (!id) throw createError({ statusCode: 400, data: { error: "ID_REQUIRED" } });
|
||||||
|
|
||||||
|
const updated = await confirmProtectedDeviceInstalled(id, user.id);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
data: { error: "DEVICE_NOT_FOUND_OR_REVOKED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: { status: updated.status, installedAt: updated.installedAt } };
|
||||||
|
});
|
||||||
45
backend/server/api/devices/[id]/profile.mobileconfig.get.ts
Normal file
45
backend/server/api/devices/[id]/profile.mobileconfig.get.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { getProtectedDevice } from "../../../db/protectedDevices";
|
||||||
|
import {
|
||||||
|
generateMacOSDnsProfile,
|
||||||
|
labelToSlug,
|
||||||
|
} from "../../../utils/mobileconfig";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/devices/:id/profile.mobileconfig
|
||||||
|
*
|
||||||
|
* PUBLIC — der Mac muss ohne Auth-Header zugreifen können.
|
||||||
|
* Der dnsToken im Profil IST die Device-Authentifizierung beim DoH-Server.
|
||||||
|
*
|
||||||
|
* Liefert ein macOS DNS-Over-HTTPS Konfigurationsprofil.
|
||||||
|
* Content-Type: application/x-apple-aspen-config
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const id = getRouterParam(event, "id");
|
||||||
|
if (!id) throw createError({ statusCode: 400, data: { error: "ID_REQUIRED" } });
|
||||||
|
|
||||||
|
const device = await getProtectedDevice(id);
|
||||||
|
|
||||||
|
if (!device || device.status === "revoked") {
|
||||||
|
throw createError({ statusCode: 404, data: { error: "DEVICE_NOT_FOUND" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const plist = generateMacOSDnsProfile({
|
||||||
|
deviceId: device.id,
|
||||||
|
dnsToken: device.dnsToken,
|
||||||
|
label: device.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
const slug = labelToSlug(device.label);
|
||||||
|
const filename = `rebreak-${slug || "schutz"}.mobileconfig`;
|
||||||
|
|
||||||
|
setHeader(event, "Content-Type", "application/x-apple-aspen-config");
|
||||||
|
setHeader(
|
||||||
|
event,
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="${filename}"`,
|
||||||
|
);
|
||||||
|
// Kein Caching — Token ist sensitiv
|
||||||
|
setHeader(event, "Cache-Control", "no-store");
|
||||||
|
|
||||||
|
return plist;
|
||||||
|
});
|
||||||
36
backend/server/api/devices/[id]/revoke.delete.ts
Normal file
36
backend/server/api/devices/[id]/revoke.delete.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { revokeProtectedDevice } from "../../../db/protectedDevices";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/devices/:id/revoke
|
||||||
|
*
|
||||||
|
* User entfernt ein Protected-Device aus der App.
|
||||||
|
* Soft-delete: status=revoked, revokedAt=NOW(). Kein hard-delete (DSGVO-Audit-Trail).
|
||||||
|
*
|
||||||
|
* WICHTIG: Das Profil bleibt auf dem Mac installiert bis der User es manuell
|
||||||
|
* entfernt. Das Backend kann das nicht aus der Ferne erzwingen (Phase 2: MDM).
|
||||||
|
*
|
||||||
|
* Auth: requireUser + ownership (revokeProtectedDevice prüft userId).
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event);
|
||||||
|
const id = getRouterParam(event, "id");
|
||||||
|
if (!id) throw createError({ statusCode: 400, data: { error: "ID_REQUIRED" } });
|
||||||
|
|
||||||
|
const revoked = await revokeProtectedDevice(id, user.id);
|
||||||
|
|
||||||
|
if (!revoked) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
data: { error: "DEVICE_NOT_FOUND_OR_ALREADY_REVOKED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
manualRemovalRequired: true,
|
||||||
|
data: {
|
||||||
|
error: "MANUAL_REMOVAL_REQUIRED",
|
||||||
|
// Frontend übersetzt diesen Code in die passende UI-Meldung
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
76
backend/server/api/devices/enroll.post.ts
Normal file
76
backend/server/api/devices/enroll.post.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { getProfile } from "../../db/profile";
|
||||||
|
import {
|
||||||
|
countActiveProtectedDevices,
|
||||||
|
createProtectedDevice,
|
||||||
|
} from "../../db/protectedDevices";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/devices/enroll
|
||||||
|
*
|
||||||
|
* Legend-only. User klickt "Mac hinzufügen" in der App.
|
||||||
|
* Legt ein ProtectedDevice (status=pending) an und gibt die Download-URL
|
||||||
|
* für das mobileconfig-Profil zurück.
|
||||||
|
*
|
||||||
|
* Body: { platform: "mac" | "windows" | "ios" | "android", label: string }
|
||||||
|
* Response: { deviceId, dnsToken, downloadUrl }
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event);
|
||||||
|
|
||||||
|
const profile = await getProfile(user.id);
|
||||||
|
if (profile?.plan !== "legend") {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
data: { error: "LEGEND_REQUIRED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event);
|
||||||
|
const platform = body?.platform as string | undefined;
|
||||||
|
const label = body?.label as string | undefined;
|
||||||
|
|
||||||
|
const VALID_PLATFORMS = ["mac", "windows", "ios", "android"];
|
||||||
|
if (!platform || !VALID_PLATFORMS.includes(platform)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "INVALID_PLATFORM", validValues: VALID_PLATFORMS },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!label || typeof label !== "string" || label.trim().length === 0) {
|
||||||
|
throw createError({ statusCode: 400, data: { error: "LABEL_REQUIRED" } });
|
||||||
|
}
|
||||||
|
const trimmedLabel = label.trim().slice(0, 100);
|
||||||
|
|
||||||
|
// Limit: max 3 active+pending Devices
|
||||||
|
const activeCount = await countActiveProtectedDevices(user.id);
|
||||||
|
if (activeCount >= 3) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 409,
|
||||||
|
data: { error: "DEVICE_LIMIT_REACHED", max: 3, current: activeCount },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32-char hex token — kryptografisch sicher
|
||||||
|
const dnsToken = randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const device = await createProtectedDevice({
|
||||||
|
userId: user.id,
|
||||||
|
dnsToken,
|
||||||
|
platform,
|
||||||
|
label: trimmedLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = useRuntimeConfig(event);
|
||||||
|
const apiBase =
|
||||||
|
(config.public as any)?.apiBase ?? "https://api.rebreak.org";
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
deviceId: device.id,
|
||||||
|
dnsToken: device.dnsToken,
|
||||||
|
downloadUrl: `${apiBase}/api/devices/${device.id}/profile.mobileconfig`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
27
backend/server/api/devices/protected.get.ts
Normal file
27
backend/server/api/devices/protected.get.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { getProfile } from "../../db/profile";
|
||||||
|
import { listProtectedDevices } from "../../db/protectedDevices";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/devices/protected
|
||||||
|
*
|
||||||
|
* Liste aller aktiven+pending ProtectedDevices des Users.
|
||||||
|
* Niemals dnsToken zurückgeben — Security.
|
||||||
|
*
|
||||||
|
* Auth: requireUser
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event);
|
||||||
|
const profile = await getProfile(user.id);
|
||||||
|
|
||||||
|
const devices = await listProtectedDevices(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
devices,
|
||||||
|
plan: profile?.plan ?? "free",
|
||||||
|
max: 3,
|
||||||
|
isLegend: profile?.plan === "legend",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
139
backend/server/db/protectedDevices.ts
Normal file
139
backend/server/db/protectedDevices.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { usePrisma } from "../utils/prisma";
|
||||||
|
|
||||||
|
export interface ProtectedDeviceRecord {
|
||||||
|
id: string;
|
||||||
|
platform: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
installedAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtectedDeviceWithToken extends ProtectedDeviceRecord {
|
||||||
|
dnsToken: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alle nicht-revoked Devices eines Users, neueste zuerst. */
|
||||||
|
export async function listProtectedDevices(
|
||||||
|
userId: string,
|
||||||
|
): Promise<ProtectedDeviceRecord[]> {
|
||||||
|
const db = usePrisma();
|
||||||
|
return db.protectedDevice.findMany({
|
||||||
|
where: { userId, status: { not: "revoked" } },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
platform: true,
|
||||||
|
label: true,
|
||||||
|
status: true,
|
||||||
|
installedAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzahl der aktiven+pending Devices für Limit-Check. */
|
||||||
|
export async function countActiveProtectedDevices(
|
||||||
|
userId: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const db = usePrisma();
|
||||||
|
return db.protectedDevice.count({
|
||||||
|
where: { userId, status: { in: ["active", "pending"] } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lookup by id — inkl. dnsToken und userId (für mobileconfig-Generation + ownership-check). */
|
||||||
|
export async function getProtectedDevice(
|
||||||
|
id: string,
|
||||||
|
): Promise<ProtectedDeviceWithToken | null> {
|
||||||
|
const db = usePrisma();
|
||||||
|
return db.protectedDevice.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
dnsToken: true,
|
||||||
|
platform: true,
|
||||||
|
label: true,
|
||||||
|
status: true,
|
||||||
|
installedAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anlegen eines neuen ProtectedDevice (status=pending). */
|
||||||
|
export async function createProtectedDevice(opts: {
|
||||||
|
userId: string;
|
||||||
|
dnsToken: string;
|
||||||
|
platform: string;
|
||||||
|
label: string;
|
||||||
|
}): Promise<ProtectedDeviceWithToken> {
|
||||||
|
const db = usePrisma();
|
||||||
|
return db.protectedDevice.create({
|
||||||
|
data: {
|
||||||
|
userId: opts.userId,
|
||||||
|
dnsToken: opts.dnsToken,
|
||||||
|
platform: opts.platform,
|
||||||
|
label: opts.label,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
dnsToken: true,
|
||||||
|
platform: true,
|
||||||
|
label: true,
|
||||||
|
status: true,
|
||||||
|
installedAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User bestätigt Installation — setzt installedAt + status=active. */
|
||||||
|
export async function confirmProtectedDeviceInstalled(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<ProtectedDeviceRecord | null> {
|
||||||
|
const db = usePrisma();
|
||||||
|
const device = await db.protectedDevice.findFirst({
|
||||||
|
where: { id, userId, status: { not: "revoked" } },
|
||||||
|
});
|
||||||
|
if (!device) return null;
|
||||||
|
|
||||||
|
return db.protectedDevice.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
status: "active",
|
||||||
|
installedAt: new Date(),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
platform: true,
|
||||||
|
label: true,
|
||||||
|
status: true,
|
||||||
|
installedAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete: setzt status=revoked + revokedAt. Ownership-check via userId. */
|
||||||
|
export async function revokeProtectedDevice(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const db = usePrisma();
|
||||||
|
const device = await db.protectedDevice.findFirst({
|
||||||
|
where: { id, userId, status: { not: "revoked" } },
|
||||||
|
});
|
||||||
|
if (!device) return false;
|
||||||
|
|
||||||
|
await db.protectedDevice.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: "revoked", revokedAt: new Date() },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
103
backend/server/utils/mobileconfig.ts
Normal file
103
backend/server/utils/mobileconfig.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
/** XML-Escape für User-Input in plist-Strings. */
|
||||||
|
function xmlEscape(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slugify label für Content-Disposition filename. */
|
||||||
|
export function labelToSlug(label: string): string {
|
||||||
|
return label
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.slice(0, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert ein macOS DNS-Over-HTTPS Konfigurationsprofil (plist XML).
|
||||||
|
*
|
||||||
|
* Jeder Aufruf erzeugt neue random PayloadUUIDs — damit der Mac mehrere Profile
|
||||||
|
* unterscheiden kann falls ein User das Profil neu lädt.
|
||||||
|
*
|
||||||
|
* Phase 2: DoH-Server routet per dnsToken auf user-spezifische Blocklist.
|
||||||
|
*/
|
||||||
|
export function generateMacOSDnsProfile(opts: {
|
||||||
|
deviceId: string;
|
||||||
|
dnsToken: string;
|
||||||
|
label: string;
|
||||||
|
}): string {
|
||||||
|
const { deviceId, dnsToken, label } = opts;
|
||||||
|
|
||||||
|
// Unique UUIDs pro Generierung (nicht deviceId verwenden — Mac dedupliciert sonst)
|
||||||
|
const outerUUID = randomUUID().toUpperCase();
|
||||||
|
const innerUUID = randomUUID().toUpperCase();
|
||||||
|
|
||||||
|
// PayloadIdentifier: stabil pro Device (kein UUID hier — bleibt gleich bei Re-Download)
|
||||||
|
const tokenPrefix = dnsToken.slice(0, 8);
|
||||||
|
const payloadId = `org.rebreak.protection.dns.${tokenPrefix}`;
|
||||||
|
|
||||||
|
const displayName = xmlEscape(`ReBreak Schutz — ${label}`);
|
||||||
|
const innerDisplayName = xmlEscape("ReBreak DNS-Filter");
|
||||||
|
const description = xmlEscape(
|
||||||
|
"Leitet DNS-Anfragen über dns.rebreak.org. Glücksspiel-Domains werden blockiert.",
|
||||||
|
);
|
||||||
|
const outerDescription = xmlEscape(
|
||||||
|
"Aktiviert den ReBreak-DNS-Filter auf diesem Mac. Glücksspiel-Domains werden auf System-Ebene blockiert — gilt für alle Browser und alle Apps. Entfernen erfordert Admin-Passwort.",
|
||||||
|
);
|
||||||
|
const serverURL = `https://dns.rebreak.org/api/dns/${dnsToken}/dns-query`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>${innerDisplayName}</string>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>${description}</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>${payloadId}.dns</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.dnsSettings.managed</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>${innerUUID}</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>DNSSettings</key>
|
||||||
|
<dict>
|
||||||
|
<key>DNSProtocol</key>
|
||||||
|
<string>HTTPS</string>
|
||||||
|
<key>ServerURL</key>
|
||||||
|
<string>${serverURL}</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>${displayName}</string>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>${outerDescription}</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>${payloadId}</string>
|
||||||
|
<key>PayloadOrganization</key>
|
||||||
|
<string>ReBreak</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>${outerUUID}</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>`;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user