feat(pi-hole): support no-auth (#4480)

This commit is contained in:
Meier Lukas
2025-11-18 21:05:00 +01:00
committed by GitHub
parent 8ea6cffb53
commit c89e35e13c
3 changed files with 32 additions and 15 deletions

View File

@@ -145,7 +145,7 @@ export const integrationDefs = {
}, },
piHole: { piHole: {
name: "Pi-hole", name: "Pi-hole",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"], []],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"], category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"), documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),

View File

@@ -17,7 +17,7 @@ import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } fr
const localLogger = logger.child({ module: "PiHoleIntegrationV6" }); const localLogger = logger.child({ module: "PiHoleIntegrationV6" });
export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration { export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration {
private readonly sessionStore: SessionStore<string>; private readonly sessionStore: SessionStore<{ sid: string | null }>;
constructor(integration: IntegrationInput) { constructor(integration: IntegrationInput) {
super(integration); super(integration);
@@ -28,7 +28,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
}); });
}); });
@@ -46,7 +46,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), { return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
}); });
}); });
@@ -85,7 +85,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
body: JSON.stringify({ blocking: true }), body: JSON.stringify({ blocking: true }),
method: "POST", method: "POST",
@@ -101,7 +101,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
body: JSON.stringify({ blocking: false, timer: duration }), body: JSON.stringify({ blocking: false, timer: duration }),
method: "POST", method: "POST",
@@ -118,12 +118,16 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* @param callback * @param callback
* @returns * @returns
*/ */
private async withAuthAsync(callback: (sessionId: string) => Promise<UndiciResponse>) { private async withAuthAsync(callback: (sessionId: string | null) => Promise<UndiciResponse>) {
if (!super.hasSecretValue("apiKey")) {
return await callback(null);
}
const storedSession = await this.sessionStore.getAsync(); const storedSession = await this.sessionStore.getAsync();
if (storedSession) { if (storedSession) {
localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
const response = await callback(storedSession); const response = await callback(storedSession.sid);
if (response.status !== 401) { if (response.status !== 401) {
return response; return response;
} }
@@ -132,7 +136,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
} }
const sessionId = await this.getSessionAsync(); const sessionId = await this.getSessionAsync();
await this.sessionStore.setAsync(sessionId); await this.sessionStore.setAsync({ sid: sessionId });
const response = await callback(sessionId); const response = await callback(sessionId);
return response; return response;
} }
@@ -141,11 +145,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* Get a session id from the Pi-hole server * Get a session id from the Pi-hole server
* @returns The session id * @returns The session id
*/ */
private async getSessionAsync(fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync): Promise<string> { private async getSessionAsync(
const apiKey = super.getSecretValue("apiKey"); fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
): Promise<string | null> {
const apiKey = super.hasSecretValue("apiKey") ? super.getSecretValue("apiKey") : null;
const response = await fetchAsync(this.url("/api/auth"), { const response = await fetchAsync(this.url("/api/auth"), {
method: "POST", method: "POST",
body: JSON.stringify({ password: apiKey }), body: JSON.stringify({ password: apiKey ?? "" }),
headers: { headers: {
"User-Agent": "Homarr", "User-Agent": "Homarr",
}, },
@@ -156,8 +162,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const data = await response.json(); const data = await response.json();
const result = await sessionResponseSchema.parseAsync(data); const result = await sessionResponseSchema.parseAsync(data);
if (!result.session.sid) { if (!result.session.valid) {
throw new ResponseError({ status: 401, url: response.url }); throw new ResponseError(
{ status: 401, url: response.url },
{
cause: result.session.message ? new Error(result.session.message) : undefined,
},
);
} }
localLogger.info("Received session id successfully", { integrationId: this.integration.id }); localLogger.info("Received session id successfully", { integrationId: this.integration.id });
@@ -170,9 +181,14 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* @param sessionId The session id to remove * @param sessionId The session id to remove
*/ */
private async clearSessionAsync( private async clearSessionAsync(
sessionId: string, sessionId: string | null,
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync, fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
) { ) {
if (!sessionId) {
localLogger.debug("No session id to clear");
return;
}
const response = await fetchAsync(this.url("/api/auth"), { const response = await fetchAsync(this.url("/api/auth"), {
method: "DELETE", method: "DELETE",
headers: { headers: {

View File

@@ -2,6 +2,7 @@ import { z } from "zod/v4";
export const sessionResponseSchema = z.object({ export const sessionResponseSchema = z.object({
session: z.object({ session: z.object({
valid: z.boolean(),
sid: z.string().nullable(), sid: z.string().nullable(),
message: z.string().nullable(), message: z.string().nullable(),
}), }),