chahinebrini 547f86187b fix(magic): createAdGuardClient idempotent — 400 → clients/update
Verwaiste AdGuard-Clients (magic_<deviceId> existiert, aber DB-Row fehlt nach
Crash zwischen clients/add und DB-Upsert) führten beim Re-Register zu 400 → 502.
Jetzt: bei 400 auf clients/update zurückfallen und den bestehenden Client auf
die frisch generierte clientId umbiegen. Behebt Magic-Register-502.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:45:28 +02:00

146 lines
4.3 KiB
TypeScript

/**
* AdGuard Home API Client für RebreakMagic DNS-over-HTTPS Client-Provisioning.
* Docs: https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi
*/
export interface AdGuardClientOptions {
use_global_settings?: boolean;
filtering_enabled?: boolean;
parental_enabled?: boolean;
safebrowsing_enabled?: boolean;
safesearch_enabled?: boolean;
blocked_services?: string[];
upstreams?: string[];
tags?: string[];
}
interface AdGuardClientPayload {
name: string;
ids: string[];
use_global_settings?: boolean;
filtering_enabled?: boolean;
parental_enabled?: boolean;
safebrowsing_enabled?: boolean;
safesearch_enabled?: boolean;
blocked_services?: string[];
upstreams?: string[];
tags?: string[];
}
/**
* Erstellt einen AdGuard Persistent Client mit gegebener Client-ID (DNS-Token).
* AdGuard nutzt die Client-ID im DoH-URL-Path: /dns-query/{clientId}
*
* @param name - Interner Client-Name (z.B. "magic_<deviceId>")
* @param clientId - DNS-Token (wird in DoH URL embedded)
* @param options - Filtering/Blocking-Optionen
*/
export async function createAdGuardClient(
name: string,
clientId: string,
options: AdGuardClientOptions = {},
): Promise<void> {
const config = useRuntimeConfig();
const baseUrl = config.adguardBaseUrl || "https://dns.rebreak.org";
const user = config.adguardUser;
const password = config.adguardPassword;
if (!user || !password) {
throw createError({
statusCode: 500,
message: "ADGUARD_USER and ADGUARD_PASSWORD required for Magic features",
});
}
const payload: AdGuardClientPayload = {
name,
ids: [clientId],
...options,
};
const authHeader = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
const headers = {
Authorization: authHeader,
"Content-Type": "application/json",
};
try {
const response = await $fetch(`${baseUrl}/control/clients/add`, {
method: "POST",
headers,
body: payload,
});
return response as void;
} catch (err: any) {
// AdGuard antwortet mit 400, wenn schon ein Client mit diesem Namen ODER
// dieser Client-ID existiert. Das passiert bei Re-Registrierung wenn ein
// früherer Versuch den Client anlegte, aber die DB-Row nie geschrieben wurde
// (z.B. Prozess-Crash zwischen clients/add und DB-Upsert). Statt hart zu
// failen → idempotent auf clients/update zurückfallen und den bestehenden
// Client auf den frisch generierten clientId umbiegen.
const status = err?.status ?? err?.response?.status ?? err?.statusCode;
if (status === 400) {
try {
const response = await $fetch(`${baseUrl}/control/clients/update`, {
method: "POST",
headers,
body: { name, data: payload },
});
return response as void;
} catch (updErr: any) {
console.error(
"[AdGuard] Client update (after add-conflict) failed:",
updErr,
);
throw createError({
statusCode: 502,
message: `AdGuard API error (update): ${updErr.message || "unknown"}`,
});
}
}
console.error("[AdGuard] Client creation failed:", err);
throw createError({
statusCode: 502,
message: `AdGuard API error: ${err.message || "unknown"}`,
});
}
}
/**
* Löscht einen AdGuard Persistent Client.
* @param name - Interner Client-Name (z.B. "magic_<deviceId>")
*/
export async function deleteAdGuardClient(name: string): Promise<void> {
const config = useRuntimeConfig();
const baseUrl = config.adguardBaseUrl || "https://dns.rebreak.org";
const user = config.adguardUser;
const password = config.adguardPassword;
if (!user || !password) {
throw createError({
statusCode: 500,
message: "ADGUARD_USER and ADGUARD_PASSWORD required for Magic features",
});
}
const authHeader = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
try {
const response = await $fetch(`${baseUrl}/control/clients/delete`, {
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
},
body: { name },
});
return response as void;
} catch (err: any) {
console.error("[AdGuard] Client deletion failed:", err);
throw createError({
statusCode: 502,
message: `AdGuard API error: ${err.message || "unknown"}`,
});
}
}