Files
homarr/packages/api/src/router/integration/integration-test-connection.ts
Meier Lukas 579dd5763d feat(logs): improve logs by logging errors with causes and metadata (#2703)
* feat(logs): improve logs by logging errors with causes and metadata

* fix: deepsource issue
2025-03-26 21:53:51 +01:00

113 lines
3.8 KiB
TypeScript

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 { createIntegrationAsync, IntegrationTestConnectionError } from "@homarr/integrations";
import { logger } from "@homarr/log";
type FormIntegration = Integration & {
secrets: {
kind: IntegrationSecretKind;
value: string | null;
}[];
};
export const testConnectionAsync = async (
integration: FormIntegration,
dbSecrets: {
kind: IntegrationSecretKind;
value: `${string}.${string}`;
}[] = [],
) => {
const formSecrets = integration.secrets
.filter((secret) => secret.value !== null)
.map((secret) => ({
...secret,
// We ensured above that the value is not null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: secret.value!,
source: "form" as const,
}));
const decryptedDbSecrets = dbSecrets
.map((secret) => {
try {
return {
...secret,
value: decryptSecret(secret.value),
source: "db" as const,
};
} catch (error) {
logger.warn(
new Error(
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
{ cause: error },
),
);
return null;
}
})
.filter((secret) => secret !== null);
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
const decryptedSecrets = secretKinds
.map((kind) => {
const secrets = sourcedSecrets.filter((secret) => secret.kind === kind);
// Will never be undefined because of the check before
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (secrets.length === 1) return secrets[0]!;
// There will always be a matching secret because of the getSecretKindOption function
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return secrets.find((secret) => secret.source === "form") ?? secrets[0]!;
})
.map(({ source: _, ...secret }) => secret);
const { secrets: _, ...baseIntegration } = integration;
const integrationInstance = await createIntegrationAsync({
...baseIntegration,
decryptedSecrets,
});
await integrationInstance.testConnectionAsync();
};
interface SourcedIntegrationSecret {
kind: IntegrationSecretKind;
value: string;
source: "db" | "form";
}
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
const matchingSecretKindOptions = getAllSecretKindOptions(kind).filter((secretKinds) =>
secretKinds.every((kind) => sourcedSecrets.some((secret) => secret.kind === kind)),
);
if (matchingSecretKindOptions.length === 0) {
throw new IntegrationTestConnectionError("secretNotDefined");
}
if (matchingSecretKindOptions.length === 1) {
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return matchingSecretKindOptions[0]!;
}
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"),
);
if (onlyFormSecretsKindOptions.length >= 1) {
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return onlyFormSecretsKindOptions[0]!;
}
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return matchingSecretKindOptions[0]!;
};