feat(pihole): add support for v6 (#2448)

* feat(pihole): add support for v6

* fix: add session-store to keep using same session for pi-hole requests

* chore: address pull request feedback

* fix: import issue

* fix: other import errors
This commit is contained in:
Meier Lukas
2025-03-04 21:17:35 +01:00
committed by GitHub
parent e88b29dc9d
commit 3804d530ec
33 changed files with 624 additions and 98 deletions

View File

@@ -23,7 +23,7 @@ import {
integrationKinds,
integrationSecretKindObject,
} from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { validation } from "@homarr/validation";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -465,7 +465,7 @@ export const integrationRouter = createTRPCRouter({
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
.input(z.object({ integrationId: z.string(), query: z.string() }))
.query(async ({ ctx, input }) => {
const integrationInstance = integrationCreator(ctx.integration);
const integrationInstance = await createIntegrationAsync(ctx.integration);
return await integrationInstance.searchAsync(encodeURI(input.query));
}),
});

View File

@@ -4,7 +4,7 @@ import { decryptSecret } from "@homarr/common/server";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions } from "@homarr/definitions";
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";
import { createIntegrationAsync, IntegrationTestConnectionError } from "@homarr/integrations";
import { logger } from "@homarr/log";
type FormIntegration = Integration & {
@@ -66,7 +66,7 @@ export const testConnectionAsync = async (
const { secrets: _, ...baseIntegration } = integration;
const integrationInstance = integrationCreator({
const integrationInstance = await createIntegrationAsync({
...baseIntegration,
decryptedSecrets,
});

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { validation } from "@homarr/validation";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -134,14 +134,14 @@ export const searchEngineRouter = createTRPCRouter({
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(validation.common.mediaRequestOptions)
.query(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
const integration = await createIntegrationAsync(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}),
requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(validation.common.requestMedia)
.mutation(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
const integration = await createIntegrationAsync(ctx.integration);
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
}),
create: permissionRequiredProcedure

View File

@@ -18,11 +18,13 @@ vi.mock("@homarr/common/server", async (importActual) => {
describe("testConnectionAsync should run test connection of integration", () => {
test("with input of only form secrets matching api key kind it should use form apiKey", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegration);
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([["apiKey"]]);
const integration = {
@@ -58,11 +60,13 @@ describe("testConnectionAsync should run test connection of integration", () =>
test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegration);
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([["apiKey"]]);
const integration = {
@@ -105,11 +109,13 @@ describe("testConnectionAsync should run test connection of integration", () =>
test("with input of form and db secrets matching api key kind it should use form apiKey", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegration);
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([["apiKey"]]);
const integration = {
@@ -152,11 +158,13 @@ describe("testConnectionAsync should run test connection of integration", () =>
test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegration);
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
const integration = {
@@ -203,11 +211,13 @@ describe("testConnectionAsync should run test connection of integration", () =>
test("with input of null form apiKey and db secrets for username and password it should use db username and password when both is allowed", async () => {
// Arrange
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegration);
factorySpy.mockReturnValue(
Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(),
} as homarrIntegrations.PiHoleIntegrationV6),
);
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
const integration = {

View File

@@ -1,12 +1,12 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import type { DnsHoleSummary } from "@homarr/integrations/types";
import { controlsInputSchema } from "@homarr/integrations/types";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -65,7 +65,7 @@ export const dnsHoleRouter = createTRPCRouter({
enable: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration } }) => {
const client = integrationCreator(integration);
const client = await createIntegrationAsync(integration);
await client.enableAsync();
const innerHandler = dnsHoleRequestHandler.handler(integration, {});
@@ -76,10 +76,14 @@ export const dnsHoleRouter = createTRPCRouter({
}),
disable: protectedProcedure
.input(controlsInputSchema)
.input(
z.object({
duration: z.number().optional(),
}),
)
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration }, input }) => {
const client = integrationCreator(integration);
const client = await createIntegrationAsync(integration);
await client.disableAsync(input.duration);
const innerHandler = dnsHoleRequestHandler.handler(integration, {});

View File

@@ -6,7 +6,7 @@ import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations";
import { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
import type { IntegrationAction } from "../../middlewares/integration";
@@ -69,7 +69,7 @@ export const downloadsRouter = createTRPCRouter({
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.pauseQueueAsync();
}),
);
@@ -80,7 +80,7 @@ export const downloadsRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.pauseItemAsync(input.item);
}),
);
@@ -90,7 +90,7 @@ export const downloadsRouter = createTRPCRouter({
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.resumeQueueAsync();
}),
);
@@ -101,7 +101,7 @@ export const downloadsRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.resumeItemAsync(input.item);
}),
);
@@ -112,7 +112,7 @@ export const downloadsRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.deleteItemAsync(input.item, input.fromDisk);
}),
);

View File

@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import type { Indexer } from "@homarr/integrations/types";
import { logger } from "@homarr/log";
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
@@ -59,7 +59,7 @@ export const indexerManagerRouter = createTRPCRouter({
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const client = integrationCreator(integration);
const client = await createIntegrationAsync(integration);
await client.testAllAsync().catch((err) => {
logger.error("indexer-manager router - ", err);
throw new TRPCError({

View File

@@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import type { MediaRequest } from "@homarr/integrations/types";
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
@@ -94,7 +94,7 @@ export const mediaRequestsRouter = createTRPCRouter({
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
.input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) }))
.mutation(async ({ ctx: { integration }, input }) => {
const integrationInstance = integrationCreator(integration);
const integrationInstance = await createIntegrationAsync(integration);
const innerHandler = mediaRequestListRequestHandler.handler(integration, {});
if (input.answer === "approve") {

View File

@@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state";
import type { IntegrationAction } from "../../middlewares/integration";
@@ -45,7 +45,7 @@ export const smartHomeRouter = createTRPCRouter({
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ entityId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => {
const client = integrationCreator(integration);
const client = await createIntegrationAsync(integration);
const success = await client.triggerToggleAsync(input.entityId);
const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId });
@@ -57,7 +57,7 @@ export const smartHomeRouter = createTRPCRouter({
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ automationId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => {
const client = integrationCreator(integration);
const client = await createIntegrationAsync(integration);
await client.triggerAutomationAsync(input.automationId);
}),
});