refactor: let integrations own HTTP authentication

This commit is contained in:
ajnart committed 2026-09-12 23:57:33 +02:00
1 parent 8157a43b9b
commit db4b6bf4b5
46 files changed
+425 -447

No files matched your search

@@ -1,6 +1,7 @@
import { ParseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -10,6 +11,10 @@ import type { DnsHoleSummary } from "../interfaces/dns-hole-summary/dns-hole-sum
import { filteringStatusSchema, statsResponseSchema, statusResponseSchema } from "./adguard-home-types";
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: { Authorization: `Basic ${this.getAuthorizationHeaderValue()}` } };
}
public async getSummaryAsync(): Promise<DnsHoleSummary> {
const headers = {
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
@@ -4,6 +4,8 @@ import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { bearerAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -14,11 +16,13 @@ import type { AnchorNote, AnchorNotesListInput, AnchorNoteSummary, AnchorNoteUpd
const logger = createLogger({ module: "anchorIntegration" });
export class AnchorIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return bearerAuth(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/notes", { limit: 1 }), {
headers: {
Authorization: `Bearer ${this.getSecretValue("apiKey")}`,
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -6,6 +6,7 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -36,6 +37,10 @@ interface WarriorSnapshotState {
}
export class ArchiveTeamWarriorIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.createAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/index.html"), {
headers: this.createAuthHeaders(),
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -26,6 +27,10 @@ const mediaTypeCountField = {
>;
export class AudiobookshelfIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders(), redactValues: [] };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const healthResponse = await input.fetchAsync(this.url("/healthcheck"));
+107 -286
View File
@@ -1,296 +1,120 @@
import { isHttpIntegrationKind } from "@homarr/definitions";
import type { IntegrationKind } from "@homarr/definitions";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import {
withHttpAuthentication,
apiKeyAuth,
bearerAuth,
basicAuth,
headerAuth,
queryAuth,
noAuth,
optionalAuth,
alternativeAuth,
rpcAuth,
feedAuth,
secret,
clientAuthentication,
} from "../http-auth";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { clientAuthentication } from "../http-auth";
import type { Integration, IntegrationInput } from "./integration";
const logger = createLogger({ module: "integrationFactory" });
// Keep each import path explicit so Node and Turbopack can discover every lazy integration chunk.
const integrationCreators = {
anchor: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../anchor/anchor-integration")).AnchorIntegration(input),
bearerAuth,
),
piHole: withHttpAuthentication(
async (input: IntegrationInput) =>
(await import("../pi-hole/pi-hole-integration-factory")).createPiHoleIntegrationAsync(input),
clientAuthentication,
),
adGuardHome: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../adguard-home/adguard-home-integration")).AdGuardHomeIntegration(input),
basicAuth(),
),
technitiumDns: withHttpAuthentication(
async (input: IntegrationInput) =>
(await import("../technitium/technitium-integration-factory")).createTechnitiumDnsIntegrationAsync(input),
clientAuthentication,
),
homeAssistant: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../homeassistant/homeassistant-integration")).HomeAssistantIntegration(input),
bearerAuth,
),
jellyfin: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../jellyfin/jellyfin-integration")).JellyfinIntegration(input),
clientAuthentication,
),
plex: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../plex/plex-integration")).PlexIntegration(input),
headerAuth("X-Plex-Token"),
),
sonarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../media-organizer/sonarr/sonarr-integration")).SonarrIntegration(input),
apiKeyAuth,
),
radarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../media-organizer/radarr/radarr-integration")).RadarrIntegration(input),
apiKeyAuth,
),
sabNzbd: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/sabnzbd/sabnzbd-integration")).SabnzbdIntegration(input),
queryAuth("apikey"),
),
nzbGet: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/nzbget/nzbget-integration")).NzbGetIntegration(input),
basicAuth(),
),
qBittorrent: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/qbittorrent/qbittorrent-integration")).QBitTorrentIntegration(input),
clientAuthentication,
),
deluge: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/deluge/deluge-integration")).DelugeIntegration(input),
clientAuthentication,
),
transmission: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/transmission/transmission-integration")).TransmissionIntegration(input),
clientAuthentication,
),
slskd: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/slskd/slskd-integration")).SlskdIntegration(input),
apiKeyAuth,
),
aria2: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../download-client/aria2/aria2-integration")).Aria2Integration(input),
optionalAuth(rpcAuth),
),
jellyseerr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../jellyseerr/jellyseerr-integration")).JellyseerrIntegration(input),
apiKeyAuth,
),
seerr: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../seerr/seerr-integration")).SeerrIntegration(input),
apiKeyAuth,
),
overseerr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../overseerr/overseerr-integration")).OverseerrIntegration(input),
apiKeyAuth,
),
prowlarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../prowlarr/prowlarr-integration")).ProwlarrIntegration(input),
apiKeyAuth,
),
openmediavault: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../openmediavault/openmediavault-integration")).OpenMediaVaultIntegration(input),
clientAuthentication,
),
lidarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../media-organizer/lidarr/lidarr-integration")).LidarrIntegration(input),
apiKeyAuth,
),
readarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../media-organizer/readarr/readarr-integration")).ReadarrIntegration(input),
apiKeyAuth,
),
dashDot: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../dashdot/dashdot-integration")).DashDotIntegration(input),
noAuth,
),
tdarr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../media-transcoding/tdarr-integration")).TdarrIntegration(input),
optionalAuth(apiKeyAuth),
),
proxmox: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../proxmox/proxmox-integration")).ProxmoxIntegration(input),
(input) => ({
headers: {
Authorization: `PVEAPIToken=${secret(input, "username")}@${secret(input, "realm")}!${secret(input, "tokenId")}=${secret(input, "apiKey")}`,
},
}),
),
emby: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../emby/emby-integration")).EmbyIntegration(input),
headerAuth("X-Emby-Token"),
),
nextcloud: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../nextcloud/nextcloud.integration")).NextcloudIntegration(input),
basicAuth("username", "password", { "OCS-APIRequest": "true" }),
),
unifiController: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../unifi-controller/unifi-controller-integration")).UnifiControllerIntegration(input),
clientAuthentication,
),
opnsense: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../opnsense/opnsense-integration")).OPNsenseIntegration(input),
basicAuth("opnsenseApiKey", "opnsenseApiSecret"),
),
ical: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../ical/ical-integration")).ICalIntegration(input),
feedAuth,
),
ntfy: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../ntfy/ntfy-integration")).NTFYIntegration(input),
optionalAuth(bearerAuth),
),
gotify: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../gotify/gotify-integration")).GotifyIntegration(input),
basicAuth(),
),
anchor: async (input: IntegrationInput) =>
new (await import("../anchor/anchor-integration")).AnchorIntegration(input),
piHole: async (input: IntegrationInput) =>
(await import("../pi-hole/pi-hole-integration-factory")).createPiHoleIntegrationAsync(input),
adGuardHome: async (input: IntegrationInput) =>
new (await import("../adguard-home/adguard-home-integration")).AdGuardHomeIntegration(input),
technitiumDns: async (input: IntegrationInput) =>
(await import("../technitium/technitium-integration-factory")).createTechnitiumDnsIntegrationAsync(input),
homeAssistant: async (input: IntegrationInput) =>
new (await import("../homeassistant/homeassistant-integration")).HomeAssistantIntegration(input),
jellyfin: async (input: IntegrationInput) =>
new (await import("../jellyfin/jellyfin-integration")).JellyfinIntegration(input),
plex: async (input: IntegrationInput) => new (await import("../plex/plex-integration")).PlexIntegration(input),
sonarr: async (input: IntegrationInput) =>
new (await import("../media-organizer/sonarr/sonarr-integration")).SonarrIntegration(input),
radarr: async (input: IntegrationInput) =>
new (await import("../media-organizer/radarr/radarr-integration")).RadarrIntegration(input),
sabNzbd: async (input: IntegrationInput) =>
new (await import("../download-client/sabnzbd/sabnzbd-integration")).SabnzbdIntegration(input),
nzbGet: async (input: IntegrationInput) =>
new (await import("../download-client/nzbget/nzbget-integration")).NzbGetIntegration(input),
qBittorrent: async (input: IntegrationInput) =>
new (await import("../download-client/qbittorrent/qbittorrent-integration")).QBitTorrentIntegration(input),
deluge: async (input: IntegrationInput) =>
new (await import("../download-client/deluge/deluge-integration")).DelugeIntegration(input),
transmission: async (input: IntegrationInput) =>
new (await import("../download-client/transmission/transmission-integration")).TransmissionIntegration(input),
slskd: async (input: IntegrationInput) =>
new (await import("../download-client/slskd/slskd-integration")).SlskdIntegration(input),
aria2: async (input: IntegrationInput) =>
new (await import("../download-client/aria2/aria2-integration")).Aria2Integration(input),
jellyseerr: async (input: IntegrationInput) =>
new (await import("../jellyseerr/jellyseerr-integration")).JellyseerrIntegration(input),
seerr: async (input: IntegrationInput) => new (await import("../seerr/seerr-integration")).SeerrIntegration(input),
overseerr: async (input: IntegrationInput) =>
new (await import("../overseerr/overseerr-integration")).OverseerrIntegration(input),
prowlarr: async (input: IntegrationInput) =>
new (await import("../prowlarr/prowlarr-integration")).ProwlarrIntegration(input),
openmediavault: async (input: IntegrationInput) =>
new (await import("../openmediavault/openmediavault-integration")).OpenMediaVaultIntegration(input),
lidarr: async (input: IntegrationInput) =>
new (await import("../media-organizer/lidarr/lidarr-integration")).LidarrIntegration(input),
readarr: async (input: IntegrationInput) =>
new (await import("../media-organizer/readarr/readarr-integration")).ReadarrIntegration(input),
dashDot: async (input: IntegrationInput) =>
new (await import("../dashdot/dashdot-integration")).DashDotIntegration(input),
tdarr: async (input: IntegrationInput) =>
new (await import("../media-transcoding/tdarr-integration")).TdarrIntegration(input),
proxmox: async (input: IntegrationInput) =>
new (await import("../proxmox/proxmox-integration")).ProxmoxIntegration(input),
emby: async (input: IntegrationInput) => new (await import("../emby/emby-integration")).EmbyIntegration(input),
nextcloud: async (input: IntegrationInput) =>
new (await import("../nextcloud/nextcloud.integration")).NextcloudIntegration(input),
unifiController: async (input: IntegrationInput) =>
new (await import("../unifi-controller/unifi-controller-integration")).UnifiControllerIntegration(input),
opnsense: async (input: IntegrationInput) =>
new (await import("../opnsense/opnsense-integration")).OPNsenseIntegration(input),
ical: async (input: IntegrationInput) => new (await import("../ical/ical-integration")).ICalIntegration(input),
ntfy: async (input: IntegrationInput) => new (await import("../ntfy/ntfy-integration")).NTFYIntegration(input),
gotify: async (input: IntegrationInput) =>
new (await import("../gotify/gotify-integration")).GotifyIntegration(input),
mock: async (input: IntegrationInput) => new (await import("../mock/mock-integration")).MockIntegration(input),
truenas: async (input: IntegrationInput) =>
new (await import("../truenas/truenas-integration")).TrueNasIntegration(input),
synology: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../synology/synology-integration")).SynologyIntegration(input),
clientAuthentication,
),
unraid: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../unraid/unraid-integration")).UnraidIntegration(input),
apiKeyAuth,
),
coolify: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../coolify/coolify-integration")).CoolifyIntegration(input),
bearerAuth,
),
tracearr: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../tracearr/tracearr-integration")).TracearrIntegration(input),
bearerAuth,
),
glances: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../glances/glances-integration")).GlancesIntegration(input),
noAuth,
),
immich: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../immich/immich-integration")).ImmichIntegration(input),
apiKeyAuth,
),
paperlessNgx: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../paperless-ngx/paperless-ngx-integration")).PaperlessNgxIntegration(input),
headerAuth("Authorization", "Token "),
),
patchmon: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../patchmon/patchmon-integration")).PatchMonIntegration(input),
basicAuth("patchmonApiKey", "patchmonApiSecret"),
),
speedtestTracker: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../speedtest-tracker/speedtest-tracker-integration")).SpeedtestTrackerIntegration(input),
bearerAuth,
),
audiobookshelf: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../audiobookshelf/audiobookshelf-integration")).AudiobookshelfIntegration(input),
bearerAuth,
),
navidrome: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../navidrome/navidrome-integration")).NavidromeIntegration(input),
clientAuthentication,
),
umami: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../umami/umami-integration")).UmamiIntegration(input),
clientAuthentication,
),
gluetun: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../gluetun/gluetun-integration")).GluetunIntegration(input),
alternativeAuth(apiKeyAuth),
),
archiveTeamWarrior: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../archive-team-warrior/archive-team-warrior-integration")).ArchiveTeamWarriorIntegration(
input,
),
optionalAuth(basicAuth(), ["username", "password"]),
),
uptimeKuma: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../uptime-kuma/uptime-kuma-integration")).UptimeKumaIntegration(input),
noAuth,
),
peaNut: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../peanut/peanut-integration")).PeaNutIntegration(input),
optionalAuth(basicAuth(), ["username", "password"]),
),
beszel: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../beszel/beszel-integration")).BeszelIntegration(input),
clientAuthentication,
),
bazarr: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../bazarr/bazarr-integration")).BazarrIntegration(input),
apiKeyAuth,
),
traefik: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../traefik/traefik-integration")).TraefikIntegration(input),
clientAuthentication,
),
wud: withHttpAuthentication(
async (input: IntegrationInput) => new (await import("../wud/wud-integration")).WudIntegration(input),
optionalAuth(basicAuth(), ["username", "password"]),
),
llamacpp: withHttpAuthentication(
async (input: IntegrationInput) =>
new (await import("../llama-cpp/llamacpp-integration")).LlamacppIntegration(input),
noAuth,
),
} satisfies {
[Kind in IntegrationKind]: ((input: IntegrationInput) => Promise<Integration>) &
(Kind extends "truenas" | "mock"
? unknown
: {
authenticate(input: IntegrationInput): IntegrationHttpAuthentication | Promise<IntegrationHttpAuthentication>;
});
};
synology: async (input: IntegrationInput) =>
new (await import("../synology/synology-integration")).SynologyIntegration(input),
unraid: async (input: IntegrationInput) =>
new (await import("../unraid/unraid-integration")).UnraidIntegration(input),
coolify: async (input: IntegrationInput) =>
new (await import("../coolify/coolify-integration")).CoolifyIntegration(input),
tracearr: async (input: IntegrationInput) =>
new (await import("../tracearr/tracearr-integration")).TracearrIntegration(input),
glances: async (input: IntegrationInput) =>
new (await import("../glances/glances-integration")).GlancesIntegration(input),
immich: async (input: IntegrationInput) =>
new (await import("../immich/immich-integration")).ImmichIntegration(input),
paperlessNgx: async (input: IntegrationInput) =>
new (await import("../paperless-ngx/paperless-ngx-integration")).PaperlessNgxIntegration(input),
patchmon: async (input: IntegrationInput) =>
new (await import("../patchmon/patchmon-integration")).PatchMonIntegration(input),
speedtestTracker: async (input: IntegrationInput) =>
new (await import("../speedtest-tracker/speedtest-tracker-integration")).SpeedtestTrackerIntegration(input),
audiobookshelf: async (input: IntegrationInput) =>
new (await import("../audiobookshelf/audiobookshelf-integration")).AudiobookshelfIntegration(input),
navidrome: async (input: IntegrationInput) =>
new (await import("../navidrome/navidrome-integration")).NavidromeIntegration(input),
umami: async (input: IntegrationInput) => new (await import("../umami/umami-integration")).UmamiIntegration(input),
gluetun: async (input: IntegrationInput) =>
new (await import("../gluetun/gluetun-integration")).GluetunIntegration(input),
archiveTeamWarrior: async (input: IntegrationInput) =>
new (await import("../archive-team-warrior/archive-team-warrior-integration")).ArchiveTeamWarriorIntegration(input),
uptimeKuma: async (input: IntegrationInput) =>
new (await import("../uptime-kuma/uptime-kuma-integration")).UptimeKumaIntegration(input),
peaNut: async (input: IntegrationInput) =>
new (await import("../peanut/peanut-integration")).PeaNutIntegration(input),
beszel: async (input: IntegrationInput) =>
new (await import("../beszel/beszel-integration")).BeszelIntegration(input),
bazarr: async (input: IntegrationInput) =>
new (await import("../bazarr/bazarr-integration")).BazarrIntegration(input),
traefik: async (input: IntegrationInput) =>
new (await import("../traefik/traefik-integration")).TraefikIntegration(input),
wud: async (input: IntegrationInput) => new (await import("../wud/wud-integration")).WudIntegration(input),
llamacpp: async (input: IntegrationInput) =>
new (await import("../llama-cpp/llamacpp-integration")).LlamacppIntegration(input),
} satisfies Record<IntegrationKind, (input: IntegrationInput) => Promise<Integration>>;
type IntegrationCreators = typeof integrationCreators;
type IntegrationInstanceOfKind<TKind extends keyof IntegrationCreators> = Awaited<
@@ -341,10 +165,7 @@ export async function createIntegrationAsync(
}
}
export async function getIntegrationHttpAuthenticationAsync(
integration: IntegrationInput & { kind: IntegrationKind },
): Promise<IntegrationHttpAuthentication> {
const creator = integrationCreators[integration.kind];
if (!("authenticate" in creator)) throw new Error("This integration does not use HTTP");
return creator.authenticate(integration);
export async function getIntegrationHttpAuthenticationAsync(integration: IntegrationInput & { kind: IntegrationKind }) {
if (!isHttpIntegrationKind(integration.kind)) throw new Error("This integration does not use HTTP");
return clientAuthentication(() => createIntegrationAsync(integration));
}
@@ -7,6 +7,7 @@ import { removeTrailingSlash } from "@homarr/common";
import { createAxiosCertificateInstanceAsync, createCertificateAgentAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { HandleIntegrationErrors } from "./errors/decorator";
import { TestConnectionError } from "./test-connection/test-connection-error";
import type { TestingResult } from "./test-connection/test-connection-service";
@@ -36,6 +37,8 @@ export interface IntegrationTestingInput {
export abstract class Integration {
constructor(protected integration: IntegrationInput) {}
public abstract getHttpAuthenticationAsync(refresh?: boolean): Promise<IntegrationHttpAuthentication>;
public get publicIntegration() {
return {
id: this.integration.id,
@@ -1,5 +1,6 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -8,6 +9,10 @@ import type { BazarrBadges } from "./bazarr-types";
import { bazarrBadgesSchema, bazarrSystemStatusSchema } from "./bazarr-types";
export class BazarrIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders(), redactValues: [] };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/system/status"), {
headers: this.getAuthHeaders(),
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -32,6 +33,10 @@ import {
} from "./coolify-types";
export class CoolifyIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const healthUrl = this.url("/api/health");
const healthResponse = await input.fetchAsync(healthUrl, {});
@@ -5,6 +5,7 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
@@ -14,6 +15,10 @@ import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-mo
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
export class DashDotIntegration extends Integration implements ISystemHealthMonitoringIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return {};
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/info"));
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -4,6 +4,7 @@ import type { fetch as undiciFetch } from "undici";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
@@ -13,6 +14,27 @@ import type { DownloadClientItem } from "../../interfaces/downloads/download-cli
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
export class Aria2Integration extends Integration implements IDownloadClientIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
if (!this.hasSecretValue("apiKey") || !this.getSecretValue("apiKey")) return {};
return {
transformBody: (body) => {
const authenticate = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(authenticate);
if (!value || typeof value !== "object") throw new Error("Expected a JSON-RPC request body");
const request = value as { method?: string; methodName?: string; params?: unknown[] };
const method = request.method ?? request.methodName;
const params = request.params ?? [];
if (!Array.isArray(params)) throw new Error("JSON-RPC params must be an array");
if (method === "system.multicall") return { ...request, params: [authenticate(params[0])] };
if (method === "system.listMethods" || method === "system.listNotifications") return request;
if (typeof params[0] === "string" && params[0].startsWith("token:")) params.shift();
return { ...request, params: [`token:${this.getSecretValue("apiKey")}`, ...params] };
};
return JSON.stringify(authenticate(JSON.parse(body ?? "null")));
},
};
}
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const client = this.getClient();
const keys: (keyof Aria2Download)[] = [
@@ -4,6 +4,8 @@ import type { fetch as undiciFetch } from "undici";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { basicAuth } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
@@ -14,6 +16,10 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
import type { NzbGetClient } from "./nzbget-types";
export class NzbGetIntegration extends Integration implements IDownloadClientIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return basicAuth()(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
return {
@@ -5,6 +5,8 @@ import type { fetch as undiciFetch } from "undici";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { queryAuth } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
@@ -17,6 +19,10 @@ import { historySchema, queueSchema } from "./sabnzbd-schema";
dayjs.extend(duration);
export class SabnzbdIntegration extends Integration implements IDownloadClientIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return queryAuth("apikey")(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
//This is the one call that uses the least amount of data while requiring the api key
await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" });
@@ -6,6 +6,8 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/h
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { apiKeyAuth } from "../../http-auth";
import type { IntegrationTestingInput } from "../../base/integration";
import { Integration } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
@@ -49,6 +51,10 @@ const logger = createLogger({ module: "slskd-integration" });
* Slskd integration cannot pause or stop downloads, it can only list downloads.
*/
export class SlskdIntegration extends Integration implements IDownloadClientIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
// eslint-disable-next-line no-restricted-syntax
public pauseQueueAsync(): Promise<void> {
return Promise.resolve();
@@ -76,9 +82,7 @@ export class SlskdIntegration extends Integration implements IDownloadClientInte
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/v0/server"), {
headers: {
"X-API-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) {
@@ -148,9 +152,7 @@ export class SlskdIntegration extends Integration implements IDownloadClientInte
): Promise<unknown> {
const response = await fetchWithTrustedCertificatesAsync(this.url(path), {
...options,
headers: {
"X-API-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) {
@@ -4,6 +4,8 @@ import { z } from "zod/v4";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { headerAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -90,6 +92,10 @@ const userSchema = z.object({
});
export class EmbyIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return headerAuth("X-Emby-Token")(this.integration);
}
private static readonly apiKeyHeader = "X-Emby-Token";
private static readonly deviceId = "homarr-emby-integration";
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
@@ -4,6 +4,7 @@ import { z } from "zod/v4";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { SessionStore } from "../base/session-store";
@@ -14,6 +15,10 @@ import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-mo
import type { SystemHealthMonitoring } from "../types";
export class GlancesIntegration extends Integration implements ISystemHealthMonitoringIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return {};
}
private readonly sessionStore: SessionStore<{ version: string }>;
constructor(integration: IntegrationInput) {
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -16,6 +17,10 @@ import {
import type { GluetunDnsStatus, GluetunPublicIp, GluetunVpnSettings, GluetunVpnStatus } from "./gluetun-types";
export class GluetunIntegration extends Integration implements VpnSummaryIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const authHeaders = this.getAuthHeaders();
const url = this.url("/v1/vpn/status");
@@ -2,6 +2,7 @@ import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ImageProxy } from "@homarr/image-proxy";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { Integration } from "../base/integration";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -10,6 +11,10 @@ import type { INotificationsIntegration } from "../interfaces/notifications/noti
import { gotifyApplicationsResponseSchema, gotifyMessagesResponseSchema } from "./gotify-schema";
export class GotifyIntegration extends Integration implements INotificationsIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getHeaders() };
}
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await input.fetchAsync(this.url("/health"), { headers: this.getHeaders() });
return { success: true };
@@ -5,6 +5,7 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/h
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -17,6 +18,10 @@ import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homea
const logger = createLogger({ module: "homeAssistantIntegration" });
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
public async getEntityStateAsync(entityId: string) {
try {
const response = await this.getAsync(`/api/states/${entityId}`);
+21 -90
View File
@@ -15,7 +15,7 @@ export interface IntegrationHttpAuthentication {
refreshAsync?(): Promise<IntegrationHttpAuthentication>;
}
export function secret(input: IntegrationInput, kind: IntegrationSecretKind) {
function secret(input: IntegrationInput, kind: IntegrationSecretKind) {
const value = input.decryptedSecrets.find((entry) => entry.kind === kind)?.value;
if (!value) throw new Error(`Integration credential ${kind} is missing`);
return value;
@@ -41,83 +41,28 @@ export const basicAuth =
export const queryAuth =
(name: string, kind: IntegrationSecretKind = "apiKey") =>
(input: IntegrationInput): IntegrationHttpAuthentication => ({ query: { [name]: secret(input, kind) } });
export const noAuth = (): IntegrationHttpAuthentication => ({});
export const optionalAuth =
(
resolve: (input: IntegrationInput) => IntegrationHttpAuthentication,
required: IntegrationSecretKind[] = ["apiKey"],
) =>
(input: IntegrationInput) => {
if (
!required.every((requiredKind) =>
input.decryptedSecrets.some(({ kind, value }) => kind === requiredKind && value),
)
)
return noAuth();
return resolve(input);
};
export const alternativeAuth =
(
apiKey: (input: IntegrationInput) => IntegrationHttpAuthentication,
fallback = optionalAuth(basicAuth(), ["username", "password"]),
) =>
(input: IntegrationInput) => {
if (input.decryptedSecrets.some(({ kind, value }) => kind === "apiKey" && value)) return apiKey(input);
return fallback(input);
};
export const rpcAuth = (input: IntegrationInput): IntegrationHttpAuthentication => ({
transformBody(body) {
const authenticate = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(authenticate);
if (!value || typeof value !== "object") throw new Error("Expected a JSON-RPC request body");
const request = value as { method?: string; methodName?: string; params?: unknown[] };
const method = request.method ?? request.methodName;
const params = request.params ?? [];
if (!Array.isArray(params)) throw new Error("JSON-RPC params must be an array");
if (method === "system.multicall") return { ...request, params: [authenticate(params[0])] };
if (method === "system.listMethods" || method === "system.listNotifications") return request;
if (typeof params[0] === "string" && params[0].startsWith("token:")) params.shift();
return { ...request, params: [`token:${secret(input, "apiKey")}`, ...params] };
};
return JSON.stringify(authenticate(JSON.parse(body ?? "null")));
},
});
export function withHttpAuthentication<T>(
create: (input: IntegrationInput) => Promise<T>,
authenticate: (
input: IntegrationInput,
create: () => Promise<T>,
) => IntegrationHttpAuthentication | Promise<IntegrationHttpAuthentication>,
) {
return Object.assign(create, {
authenticate: (input: IntegrationInput) => withAuthTimeout(() => authenticate(input, () => create(input))),
});
}
export const clientAuthentication = async <
T extends { getHttpAuthenticationAsync(refresh?: boolean): Promise<IntegrationHttpAuthentication> },
>(
_input: IntegrationInput,
create: () => Promise<T>,
) => {
const client = await create();
const resolve = async (refresh = false): Promise<IntegrationHttpAuthentication> => {
const auth = await client.getHttpAuthenticationAsync(refresh);
const values = [...Object.values(auth.headers ?? {}), ...Object.values(auth.query ?? {})];
for (const [name, value] of Object.entries(auth.headers ?? {})) {
if (name.toLowerCase() === "cookie") {
for (const cookie of value.split(";")) values.push(cookie.slice(cookie.indexOf("=") + 1).trim());
export const clientAuthentication = async (
create: () => Promise<{ getHttpAuthenticationAsync(refresh?: boolean): Promise<IntegrationHttpAuthentication> }>,
) =>
withAuthTimeout(async () => {
const client = await create();
const resolve = async (refresh = false): Promise<IntegrationHttpAuthentication> => {
const auth = await client.getHttpAuthenticationAsync(refresh);
const values = [...Object.values(auth.headers ?? {}), ...Object.values(auth.query ?? {})];
for (const [name, value] of Object.entries(auth.headers ?? {})) {
if (name.toLowerCase() === "cookie") {
for (const cookie of value.split(";")) values.push(cookie.slice(cookie.indexOf("=") + 1).trim());
}
if (name.toLowerCase() === "authorization") values.push(value.slice(value.indexOf(" ") + 1));
}
if (name.toLowerCase() === "authorization") values.push(value.slice(value.indexOf(" ") + 1));
}
return {
...auth,
redactValues: auth.redactValues ?? values,
refreshAsync: () => withAuthTimeout(() => resolve(true)),
return {
...auth,
redactValues: auth.redactValues ?? values,
refreshAsync: () => withAuthTimeout(() => resolve(true)),
};
};
};
return resolve();
};
return resolve();
});
async function withAuthTimeout<T>(resolve: () => T | Promise<T>): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
@@ -132,17 +77,3 @@ async function withAuthTimeout<T>(resolve: () => T | Promise<T>): Promise<T> {
clearTimeout(timer);
}
}
export const feedAuth = (input: IntegrationInput): IntegrationHttpAuthentication => {
const baseUrl = secret(input, "url");
const base = new URL(baseUrl);
return {
baseUrl,
redactValues: [...base.searchParams.values()],
transformUrl(url) {
// The stored feed URL is a complete endpoint, so the root request uses it exactly.
if (url.pathname === `${base.pathname}/`) url.pathname = base.pathname;
for (const [name, value] of base.searchParams) url.searchParams.set(name, value);
},
};
};
@@ -2,6 +2,7 @@ import ICAL from "ical.js";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -10,6 +11,20 @@ import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integ
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
export class ICalIntegration extends Integration implements ICalendarIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
const baseUrl = this.getSecretValue("url");
const base = new URL(baseUrl);
return {
baseUrl,
redactValues: [...base.searchParams.values()],
transformUrl(url) {
// The stored feed URL is a complete endpoint, so the root request uses it exactly.
if (url.pathname === `${base.pathname}/`) url.pathname = base.pathname;
for (const [name, value] of base.searchParams) url.searchParams.set(name, value);
},
};
}
async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
const response = await fetchWithTrustedCertificatesAsync(super.getSecretValue("url"));
const result = await response.text();
@@ -17,6 +17,8 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/h
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ImageProxy } from "@homarr/image-proxy";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -48,6 +50,10 @@ const logger = createLogger({ module: "immich-integration" });
const carouselPhotoCount = 50;
export class ImmichIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
constructor(integration: IntegrationInput) {
super(integration);
}
@@ -1,8 +1,14 @@
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import type { MediaAvailability } from "../interfaces/media-requests/media-request-types";
import { UpstreamMediaAvailability } from "../interfaces/media-requests/media-request-types";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
export class JellyseerrIntegration extends OverseerrIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
protected override mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
// Availability statuses are not exactly the same between Jellyseerr and Overseerr (Jellyseerr has "blacklisted" additionally (deleted is the same value in overseerr))
if (availability === UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted) return "blacklisted";
@@ -1,6 +1,7 @@
import { ParseError, ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -22,6 +23,10 @@ import {
const LLMACPP_REQUEST_TIMEOUT_MS = 10_000;
export class LlamacppIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return {};
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/health"), {
signal: AbortSignal.timeout(LLMACPP_REQUEST_TIMEOUT_MS),
@@ -3,6 +3,8 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { apiKeyAuth } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
@@ -14,9 +16,13 @@ import { mediaOrganizerPriorities } from "../media-organizer";
const logger = createLogger({ module: "lidarrIntegration" });
export class LidarrIntegration extends Integration implements ICalendarIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api"), {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -39,9 +45,7 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
"X-Api-Key": super.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const lidarrCalendarEvents = await z.array(lidarrCalendarEventSchema).parseAsync(await response.json());
@@ -3,6 +3,8 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { apiKeyAuth } from "../../http-auth";
import type { IntegrationTestingInput } from "../../base/integration";
import { Integration } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
@@ -17,6 +19,10 @@ import { mediaOrganizerPriorities } from "../media-organizer";
const logger = createLogger({ module: "radarrIntegration" });
export class RadarrIntegration extends Integration implements ICalendarIntegration, IMediaOrganizerIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
/**
* Gets the events in the Radarr calendar between two dates.
* @param start The start date
@@ -31,9 +37,7 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
"X-Api-Key": super.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json());
@@ -122,7 +126,7 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const data = await z
.object({
@@ -154,7 +158,7 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const data = await z
.object({
@@ -188,7 +192,7 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api"), {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -3,6 +3,8 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { apiKeyAuth } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
@@ -14,9 +16,13 @@ import { mediaOrganizerPriorities } from "../media-organizer";
const logger = createLogger({ module: "readarrIntegration" });
export class ReadarrIntegration extends Integration implements ICalendarIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api"), {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -45,9 +51,7 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
"X-Api-Key": super.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const readarrCalendarEvents = await z.array(readarrCalendarEventSchema).parseAsync(await response.json());
@@ -3,6 +3,8 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../../http-auth";
import { apiKeyAuth } from "../../http-auth";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
@@ -16,6 +18,10 @@ import { mediaOrganizerPriorities } from "../media-organizer";
const logger = createLogger({ module: "sonarrIntegration" });
export class SonarrIntegration extends Integration implements ICalendarIntegration, IMediaOrganizerIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
/**
* Gets the events in the Sonarr calendar between two dates.
* @param start The start date
@@ -33,9 +39,7 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
"X-Api-Key": super.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const sonarrCalendarEvents = await z.array(sonarrCalendarEventSchema).parseAsync(await response.json());
@@ -119,7 +123,7 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const data = await z
.object({
@@ -157,7 +161,7 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
});
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const data = await z
.object({
@@ -193,7 +197,7 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api"), {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -1,5 +1,7 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -9,12 +11,17 @@ import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "../interfaces/med
import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } from "./tdarr-validation-schemas";
export class TdarrIntegration extends Integration implements IMediaTranscodingIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
if (!this.hasSecretValue("apiKey") || !this.getSecretValue("apiKey")) return {};
return apiKeyAuth(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/v2/is-server-alive"), {
method: "POST",
headers: {
accept: "application/json",
"X-Api-Key": super.hasSecretValue("apiKey") ? super.getSecretValue("apiKey") : "",
...(await this.getHttpAuthenticationAsync()).headers,
},
});
@@ -30,7 +37,7 @@ export class TdarrIntegration extends Integration implements IMediaTranscodingIn
const headerParams = {
accept: "application/json",
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
...(await this.getHttpAuthenticationAsync()).headers,
};
const response = await fetchWithTrustedCertificatesAsync(url, {
@@ -76,7 +83,7 @@ export class TdarrIntegration extends Integration implements IMediaTranscodingIn
const url = this.url("/api/v2/get-nodes");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
...(await this.getHttpAuthenticationAsync()).headers,
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "GET",
@@ -120,7 +127,7 @@ export class TdarrIntegration extends Integration implements IMediaTranscodingIn
const url = this.url("/api/v2/client/status-tables");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
...(await this.getHttpAuthenticationAsync()).headers,
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "POST",
@@ -159,7 +166,7 @@ export class TdarrIntegration extends Integration implements IMediaTranscodingIn
const url = this.url("/api/v2/client/status-tables");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
...(await this.getHttpAuthenticationAsync()).headers,
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "POST",
@@ -1,3 +1,4 @@
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -52,6 +53,10 @@ export class MockIntegration
ISmartHomeIntegration,
IUpsSummaryIntegration
{
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
throw new Error("This integration does not use HTTP");
}
private static readonly dnsHole = new DnsHoleMockService();
private static readonly calendar = new CalendarMockService();
private static readonly downloadClient = new DownloadClientMockService();
@@ -12,6 +12,8 @@ import { ResponseError } from "@homarr/common/server";
import { createHttpsAgentAsync, fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { basicAuth } from "../http-auth";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import { integrationTsdavHttpErrorHandler } from "../base/errors/http";
import type { IntegrationTestingInput } from "../base/integration";
@@ -57,6 +59,10 @@ dayjs.extend(timezone);
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration implements ICalendarIntegration, INotificationsIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return basicAuth("username", "password", { "OCS-APIRequest": "true" })(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options));
await client.login();
@@ -168,12 +174,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
public async getNotificationsAsync(): Promise<Notification[]> {
const url = this.url("/ocs/v2.php/apps/notifications/api/v2/notifications", { format: "json" });
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
Authorization: `Basic ${Buffer.from(
`${this.getSecretValue("username")}:${this.getSecretValue("password")}`,
).toString("base64")}`,
"OCS-APIRequest": "true",
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) throw new ResponseError(response);
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { Integration } from "../base/integration";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -9,6 +10,10 @@ import type { INotificationsIntegration } from "../interfaces/notifications/noti
import { ntfyNotificationSchema } from "./ntfy-schema";
export class NTFYIntegration extends Integration implements INotificationsIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getHeaders() };
}
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
return { success: true };
@@ -17,7 +22,7 @@ export class NTFYIntegration extends Integration implements INotificationsIntegr
private getTopicURL() {
return this.url(`/${encodeURIComponent(super.getSecretValue("topic"))}/json`, { poll: 1 });
}
private getHeaders() {
private getHeaders(): Record<string, string> {
return this.hasSecretValue("apiKey") ? { Authorization: `Bearer ${super.getSecretValue("apiKey")}` } : {};
}
@@ -1,6 +1,7 @@
import { ParseError, ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
@@ -22,6 +23,10 @@ import {
} from "./opnsense-types";
export class OPNsenseIntegration extends Integration implements FirewallSummaryIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: { Authorization: this.getAuthHeaders() } };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/diagnostics/system/system_information"), {
headers: {
@@ -4,6 +4,8 @@ import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { ISearchableIntegration } from "../base/searchable-integration";
@@ -40,11 +42,13 @@ export class OverseerrIntegration
extends Integration
implements IMediaRequestIntegration, ISearchableIntegration<OverseerrSearchResult>
{
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
public async searchAsync(query: string) {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const schemaData = await searchSchema.parseAsync(await response.json());
@@ -69,9 +73,7 @@ export class OverseerrIntegration
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number) {
const url = mediaType === "tv" ? this.url(`/api/v1/tv/${id}`) : this.url(`/api/v1/movie/${id}`);
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const data = await mediaInformationSchema.parseAsync(await response.json());
const requestedSeasons = [
@@ -110,9 +112,7 @@ export class OverseerrIntegration
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/v1/auth/me"), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -126,18 +126,14 @@ export class OverseerrIntegration
const pendingRequests = await fetchWithTrustedCertificatesAsync(
this.url("/api/v1/request", { take: 20, filter: "pending" }),
{
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
},
);
const allRequests = await fetchWithTrustedCertificatesAsync(
this.url("/api/v1/request", { take: 20, sort: "modified" }),
{
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
},
);
@@ -234,18 +230,14 @@ export class OverseerrIntegration
public async getStatsAsync(): Promise<RequestStats> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
return await getStatsSchema.parseAsync(await response.json());
}
public async getUsersAsync(): Promise<RequestUser[]> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/user", { take: 10, sort: "requests" }), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
const users = (await getUsersSchema.parseAsync(await response.json())).results;
return users.map((user): RequestUser => {
@@ -261,9 +253,7 @@ export class OverseerrIntegration
logger.info("Approving media request", { requestId, integration: this.integration.name });
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), {
method: "POST",
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) throw new ResponseError(response);
logger.info("Successfully approved media request", { requestId, integration: this.integration.name });
@@ -274,9 +264,7 @@ export class OverseerrIntegration
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/decline`), {
method: "POST",
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) throw new ResponseError(response);
logger.info("Successfully declined media request", { requestId, integration: this.integration.name });
@@ -284,9 +272,7 @@ export class OverseerrIntegration
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/${type}/${id}`), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
headers: (await this.getHttpAuthenticationAsync()).headers,
});
if (!response.ok) {
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -9,6 +10,10 @@ import type { PaperlessNgxStats } from "./paperless-ngx-types";
import { paperlessNgxPaginatedCountSchema, paperlessNgxStatisticsSchema } from "./paperless-ngx-types";
export class PaperlessNgxIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const statisticsUrl = this.url("/api/statistics/");
const statisticsResponse = await input.fetchAsync(statisticsUrl, {
@@ -1,6 +1,7 @@
import { ParseError, ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -11,6 +12,10 @@ import { mapPatchMonStats, parsePatchMonStatsResponseAsync } from "./patchmon-ty
const STATS_REQUEST_TIMEOUT_MS = 10_000;
export class PatchMonIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/v1/gethomepage/stats"), {
headers: this.getAuthHeaders(),
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -11,6 +12,10 @@ import type { PeaNutDevice } from "./peanut-types";
import { peaNutDevicesSchema } from "./peanut-types";
export class PeaNutIntegration extends Integration implements IUpsSummaryIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/v1/devices"), {
headers: this.getAuthHeaders(),
@@ -6,6 +6,8 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/h
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ImageProxy } from "@homarr/image-proxy";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { headerAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -50,6 +52,10 @@ function isStreamDirect(decision: string | undefined): boolean {
}
export class PlexIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return headerAuth("X-Plex-Token")(this.integration);
}
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
const token = super.getSecretValue("apiKey");
@@ -2,6 +2,8 @@ import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -11,6 +13,10 @@ import type { Indexer } from "../interfaces/indexer-manager/indexer-manager-type
import { indexerResponseSchema, statusResponseSchema } from "./prowlarr-types";
export class ProwlarrIntegration extends Integration implements IIndexerManagerIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
public async getIndexersAsync(): Promise<Indexer[]> {
const apiKey = super.getSecretValue("apiKey");
@@ -4,6 +4,7 @@ import proxmoxApi from "proxmox-api";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
@@ -23,6 +24,14 @@ const logger = createLogger({ module: "proxmoxIntegration" });
@HandleIntegrationErrors([new ProxmoxApiErrorHandler()])
export class ProxmoxIntegration extends Integration implements IClusterHealthMonitoringIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return {
headers: {
Authorization: `PVEAPIToken=${this.getSecretValue("username")}@${this.getSecretValue("realm")}!${this.getSecretValue("tokenId")}=${this.getSecretValue("apiKey")}`,
},
};
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const proxmox = this.getPromoxApi(input.fetchAsync);
await proxmox.nodes.$get();
@@ -1,6 +1,7 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -18,6 +19,10 @@ import type {
} from "./speedtest-tracker-types";
export class SpeedtestTrackerIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders(), redactValues: [] };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const url = this.url("/api/v1/results/latest");
const response = await input.fetchAsync(url, {
@@ -3,6 +3,7 @@ import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ImageProxy } from "@homarr/image-proxy";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -17,6 +18,10 @@ import type {
} from "./tracearr-types";
export class TracearrIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const healthUrl = this.url("/api/v1/public/health");
const healthResponse = await input.fetchAsync(healthUrl, {
@@ -3,6 +3,7 @@ import z from "zod";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
@@ -32,6 +33,10 @@ const describeRequestError = (error: unknown) => {
};
export class TrueNasIntegration extends Integration implements ISystemHealthMonitoringIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
throw new Error("This integration does not use HTTP");
}
private client?: TrueNasClient;
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -5,6 +5,8 @@ import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IntegrationHttpAuthentication } from "../http-auth";
import { apiKeyAuth } from "../http-auth";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
@@ -18,6 +20,10 @@ const logger = createLogger({ module: "UnraidIntegration" });
@HandleIntegrationErrors([])
export class UnraidIntegration extends Integration implements ISystemHealthMonitoringIntegration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return apiKeyAuth(this.integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.queryGraphQLAsync<{ info: UnraidSystemInfo }>(
`
@@ -1,6 +1,7 @@
import { ParseError, ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -13,6 +14,10 @@ import {
import type { UptimeKumaDashboardData, UptimeKumaMonitor, UptimeKumaMonitorCategory } from "./uptime-kuma-types";
export class UptimeKumaIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return {};
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const statusPageResponse = await input.fetchAsync(this.statusPageUrl());
@@ -3,6 +3,7 @@ import { Buffer } from "node:buffer";
import { ParseError, ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationHttpAuthentication } from "../http-auth";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -13,6 +14,10 @@ import { mapWudStats, parseWudContainersResponseAsync } from "./wud-types";
const CONTAINERS_REQUEST_TIMEOUT_MS = 10_000;
export class WudIntegration extends Integration {
public async getHttpAuthenticationAsync(): Promise<IntegrationHttpAuthentication> {
return { headers: this.getAuthHeaders() };
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/containers"), {
headers: this.getAuthHeaders(),
+3
View File
@@ -45,6 +45,9 @@ describe("Base integration", () => {
});
class FakeIntegration extends Integration {
public async getHttpAuthenticationAsync() {
return {};
}
constructor(
private testingResult?: TestingResult,
private error?: Error,