/** * 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_") * @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 { 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_") */ export async function deleteAdGuardClient(name: string): Promise { 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"}`, }); } }