mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
refactor(certificates): move to core package (#4686)
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
import { IconCertificateOff } from "@tabler/icons-react";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getTrustedCertificateHostnamesAsync } from "@homarr/certificates/server";
|
||||
import { getTrustedCertificateHostnamesAsync } from "@homarr/core/infrastructure/certificates";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { loadCustomRootCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { getMantineColor } from "@homarr/common";
|
||||
import { loadCustomRootCertificatesAsync } from "@homarr/core/infrastructure/certificates";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
@@ -3,7 +3,10 @@ import { TRPCError } from "@trpc/server";
|
||||
import { zfd } from "zod-form-data";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||
import {
|
||||
addCustomRootCertificateAsync,
|
||||
removeCustomRootCertificateAsync,
|
||||
} from "@homarr/core/infrastructure/certificates";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
|
||||
@@ -1,93 +1,18 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import type { AgentOptions } from "node:https";
|
||||
import { Agent as HttpsAgent } from "node:https";
|
||||
import path from "node:path";
|
||||
import { checkServerIdentity, rootCertificates } from "node:tls";
|
||||
import { checkServerIdentity } from "node:tls";
|
||||
import axios from "axios";
|
||||
import type { RequestInfo, RequestInit, Response } from "undici";
|
||||
import { fetch } from "undici";
|
||||
|
||||
import { env } from "@homarr/common/env";
|
||||
import { LoggingAgent } from "@homarr/common/server";
|
||||
import {
|
||||
getAllTrustedCertificatesAsync,
|
||||
getTrustedCertificateHostnamesAsync,
|
||||
} from "@homarr/core/infrastructure/certificates";
|
||||
import type { InferSelectModel } from "@homarr/db";
|
||||
import { db } from "@homarr/db";
|
||||
import type { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||
|
||||
const getCertificateFolder = () => {
|
||||
if (env.NODE_ENV !== "production") return process.env.LOCAL_CERTIFICATE_PATH;
|
||||
return process.env.LOCAL_CERTIFICATE_PATH ?? path.join("/appdata", "trusted-certificates");
|
||||
};
|
||||
|
||||
export const loadCustomRootCertificatesAsync = async () => {
|
||||
const folder = getCertificateFolder();
|
||||
|
||||
if (!folder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!fsSync.existsSync(folder)) {
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
}
|
||||
|
||||
const dirContent = await fs.readdir(folder);
|
||||
return await Promise.all(
|
||||
dirContent
|
||||
.filter((file) => file.endsWith(".crt") || file.endsWith(".pem"))
|
||||
.map(async (file) => ({
|
||||
content: await fs.readFile(path.join(folder, file), "utf8"),
|
||||
fileName: file,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
export const removeCustomRootCertificateAsync = async (fileName: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingFiles = await fs.readdir(folder, { withFileTypes: true });
|
||||
if (!existingFiles.some((file) => file.isFile() && file.name === fileName)) {
|
||||
throw new Error(`File ${fileName} does not exist`);
|
||||
}
|
||||
|
||||
const fullPath = path.join(folder, fileName);
|
||||
const content = await fs.readFile(fullPath, "utf8");
|
||||
|
||||
await fs.rm(fullPath);
|
||||
try {
|
||||
return new X509Certificate(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const addCustomRootCertificateAsync = async (fileName: string, content: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
throw new Error(
|
||||
"When you want to use custom certificates locally you need to set LOCAL_CERTIFICATE_PATH to an absolute path",
|
||||
);
|
||||
}
|
||||
|
||||
if (fileName.includes("/")) {
|
||||
throw new Error("Invalid file name");
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(folder, fileName), content);
|
||||
};
|
||||
|
||||
export const getTrustedCertificateHostnamesAsync = async () => {
|
||||
return await db.query.trustedCertificateHostnames.findMany();
|
||||
};
|
||||
|
||||
export const getAllTrustedCertificatesAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return rootCertificates.concat(customCertificates.map((cert) => cert.content));
|
||||
};
|
||||
|
||||
export const createCustomCheckServerIdentity = (
|
||||
trustedHostnames: InferSelectModel<typeof trustedCertificateHostnames>[],
|
||||
): typeof checkServerIdentity => {
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"./infrastructure/logs/error": "./src/infrastructure/logs/error.ts",
|
||||
"./infrastructure/db": "./src/infrastructure/db/index.ts",
|
||||
"./infrastructure/db/env": "./src/infrastructure/db/env.ts",
|
||||
"./infrastructure/db/constants": "./src/infrastructure/db/constants.ts"
|
||||
"./infrastructure/db/constants": "./src/infrastructure/db/constants.ts",
|
||||
"./infrastructure/certificates": "./src/infrastructure/certificates/index.ts",
|
||||
"./infrastructure/certificates/hostnames/db/sqlite": "./src/infrastructure/certificates/hostnames/db/sqlite.ts",
|
||||
"./infrastructure/certificates/hostnames/db/mysql": "./src/infrastructure/certificates/hostnames/db/mysql.ts",
|
||||
"./infrastructure/certificates/hostnames/db/postgresql": "./src/infrastructure/certificates/hostnames/db/postgresql.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
74
packages/core/src/infrastructure/certificates/files/index.ts
Normal file
74
packages/core/src/infrastructure/certificates/files/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { rootCertificates } from "node:tls";
|
||||
|
||||
const getCertificateFolder = () => {
|
||||
if (process.env.NODE_ENV !== "production") return process.env.LOCAL_CERTIFICATE_PATH;
|
||||
return process.env.LOCAL_CERTIFICATE_PATH ?? path.join("/appdata", "trusted-certificates");
|
||||
};
|
||||
|
||||
export const loadCustomRootCertificatesAsync = async () => {
|
||||
const folder = getCertificateFolder();
|
||||
|
||||
if (!folder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!fsSync.existsSync(folder)) {
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
}
|
||||
|
||||
const dirContent = await fs.readdir(folder);
|
||||
return await Promise.all(
|
||||
dirContent
|
||||
.filter((file) => file.endsWith(".crt") || file.endsWith(".pem"))
|
||||
.map(async (file) => ({
|
||||
content: await fs.readFile(path.join(folder, file), "utf8"),
|
||||
fileName: file,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
export const getAllTrustedCertificatesAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return rootCertificates.concat(customCertificates.map((cert) => cert.content));
|
||||
};
|
||||
|
||||
export const removeCustomRootCertificateAsync = async (fileName: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingFiles = await fs.readdir(folder, { withFileTypes: true });
|
||||
if (!existingFiles.some((file) => file.isFile() && file.name === fileName)) {
|
||||
throw new Error(`File ${fileName} does not exist`);
|
||||
}
|
||||
|
||||
const fullPath = path.join(folder, fileName);
|
||||
const content = await fs.readFile(fullPath, "utf8");
|
||||
|
||||
await fs.rm(fullPath);
|
||||
try {
|
||||
return new X509Certificate(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const addCustomRootCertificateAsync = async (fileName: string, content: string) => {
|
||||
const folder = getCertificateFolder();
|
||||
if (!folder) {
|
||||
throw new Error(
|
||||
"When you want to use custom certificates locally you need to set LOCAL_CERTIFICATE_PATH to an absolute path",
|
||||
);
|
||||
}
|
||||
|
||||
if (fileName.includes("/")) {
|
||||
throw new Error("Invalid file name");
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(folder, fileName), content);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { mysqlTable, primaryKey, text, varchar } from "drizzle-orm/mysql-core";
|
||||
|
||||
export const trustedCertificateHostnames = mysqlTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: varchar({ length: 256 }).notNull(),
|
||||
thumbprint: varchar({ length: 128 }).notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { pgTable, primaryKey, text, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
export const trustedCertificateHostnames = pgTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: varchar({ length: 256 }).notNull(),
|
||||
thumbprint: varchar({ length: 128 }).notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createSchema } from "../../../db";
|
||||
import * as mysql from "./mysql";
|
||||
import * as postgresql from "./postgresql";
|
||||
import * as sqlite from "./sqlite";
|
||||
|
||||
export const schema = createSchema({
|
||||
"better-sqlite3": () => sqlite,
|
||||
mysql2: () => mysql,
|
||||
"node-postgres": () => postgresql,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const trustedCertificateHostnames = sqliteTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: text().notNull(),
|
||||
thumbprint: text().notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createDb } from "../../db";
|
||||
import { schema } from "./db/schema";
|
||||
|
||||
const db = createDb(schema);
|
||||
|
||||
export const getTrustedCertificateHostnamesAsync = async () => {
|
||||
return await db.query.trustedCertificateHostnames.findMany();
|
||||
};
|
||||
7
packages/core/src/infrastructure/certificates/index.ts
Normal file
7
packages/core/src/infrastructure/certificates/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { getTrustedCertificateHostnamesAsync } from "./hostnames";
|
||||
export {
|
||||
addCustomRootCertificateAsync,
|
||||
removeCustomRootCertificateAsync,
|
||||
getAllTrustedCertificatesAsync,
|
||||
loadCustomRootCertificatesAsync,
|
||||
} from "./files";
|
||||
@@ -46,6 +46,8 @@ const customBlob = customType<{ data: Buffer }>({
|
||||
},
|
||||
});
|
||||
|
||||
export * from "@homarr/core/infrastructure/certificates/hostnames/db/mysql";
|
||||
|
||||
export const apiKeys = mysqlTable("apiKey", {
|
||||
id: varchar({ length: 64 }).notNull().primaryKey(),
|
||||
apiKey: text().notNull(),
|
||||
@@ -495,20 +497,6 @@ export const onboarding = mysqlTable("onboarding", {
|
||||
previousStep: varchar({ length: 64 }).$type<OnboardingStep>(),
|
||||
});
|
||||
|
||||
export const trustedCertificateHostnames = mysqlTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: varchar({ length: 256 }).notNull(),
|
||||
thumbprint: varchar({ length: 128 }).notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const cronJobConfigurations = mysqlTable("cron_job_configuration", {
|
||||
name: varchar({ length: 256 }).notNull().primaryKey(),
|
||||
cronExpression: varchar({ length: 32 }).notNull(),
|
||||
|
||||
@@ -45,6 +45,8 @@ const customBlob = customType<{ data: Buffer }>({
|
||||
},
|
||||
});
|
||||
|
||||
export * from "@homarr/core/infrastructure/certificates/hostnames/db/postgresql";
|
||||
|
||||
export const apiKeys = pgTable("apiKey", {
|
||||
id: varchar({ length: 64 }).notNull().primaryKey(),
|
||||
apiKey: text().notNull(),
|
||||
@@ -494,20 +496,6 @@ export const onboarding = pgTable("onboarding", {
|
||||
previousStep: varchar({ length: 64 }).$type<OnboardingStep>(),
|
||||
});
|
||||
|
||||
export const trustedCertificateHostnames = pgTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: varchar({ length: 256 }).notNull(),
|
||||
thumbprint: varchar({ length: 128 }).notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const cronJobConfigurations = pgTable("cron_job_configuration", {
|
||||
name: varchar({ length: 256 }).notNull().primaryKey(),
|
||||
cronExpression: varchar({ length: 32 }).notNull(),
|
||||
|
||||
@@ -28,6 +28,8 @@ import type {
|
||||
WidgetKind,
|
||||
} from "@homarr/definitions";
|
||||
|
||||
export * from "@homarr/core/infrastructure/certificates/hostnames/db/sqlite";
|
||||
|
||||
export const apiKeys = sqliteTable("apiKey", {
|
||||
id: text().notNull().primaryKey(),
|
||||
apiKey: text().notNull(),
|
||||
@@ -480,20 +482,6 @@ export const onboarding = sqliteTable("onboarding", {
|
||||
previousStep: text().$type<OnboardingStep>(),
|
||||
});
|
||||
|
||||
export const trustedCertificateHostnames = sqliteTable(
|
||||
"trusted_certificate_hostname",
|
||||
{
|
||||
hostname: text().notNull(),
|
||||
thumbprint: text().notNull(),
|
||||
certificate: text().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.hostname, table.thumbprint],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const cronJobConfigurations = sqliteTable("cron_job_configuration", {
|
||||
name: text().notNull().primaryKey(),
|
||||
cronExpression: text().notNull(),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { X509Certificate } from "node:crypto";
|
||||
import tls from "node:tls";
|
||||
|
||||
import { createCustomCheckServerIdentity } from "@homarr/certificates/server";
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
import {
|
||||
createCustomCheckServerIdentity,
|
||||
getAllTrustedCertificatesAsync,
|
||||
getTrustedCertificateHostnamesAsync,
|
||||
} from "@homarr/certificates/server";
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
} from "@homarr/core/infrastructure/certificates";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
import type { IntegrationRequestErrorOfType } from "../errors/http/integration-request-error";
|
||||
|
||||
@@ -2,12 +2,12 @@ import type tls from "node:tls";
|
||||
import axios from "axios";
|
||||
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
|
||||
|
||||
import { createCustomCheckServerIdentity } from "@homarr/certificates/server";
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
import {
|
||||
createCustomCheckServerIdentity,
|
||||
getAllTrustedCertificatesAsync,
|
||||
getTrustedCertificateHostnamesAsync,
|
||||
} from "@homarr/certificates/server";
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
} from "@homarr/core/infrastructure/certificates";
|
||||
import type { SiteStats } from "@homarr/node-unifi";
|
||||
import Unifi from "@homarr/node-unifi";
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
db: createDb(),
|
||||
};
|
||||
});
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const API_KEY = "ARIA2_API_KEY";
|
||||
const IMAGE_NAME = "hurlenko/aria2-ariang:latest";
|
||||
|
||||
@@ -16,6 +16,17 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Base integration", () => {
|
||||
test("testConnectionAsync should handle errors", async () => {
|
||||
const responseError = new ResponseError({ status: 500, url: "https://example.com" });
|
||||
|
||||
@@ -17,6 +17,17 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const DEFAULT_API_KEY =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNjQwY2VjNDFjOGU0NGM5YmRlNWQ4ZmFjMjUzYWViZiIsImlhdCI6MTcxODQ3MTE1MSwiZXhwIjoyMDMzODMxMTUxfQ.uQCZ5FZTokipa6N27DtFhLHkwYEXU1LZr0fsVTryL2Q";
|
||||
const IMAGE_NAME = "ghcr.io/home-assistant/home-assistant:stable";
|
||||
|
||||
@@ -18,6 +18,17 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const username = "nzbget";
|
||||
const password = "tegbzn6789";
|
||||
const IMAGE_NAME = "linuxserver/nzbget:latest";
|
||||
|
||||
@@ -17,6 +17,17 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const DEFAULT_PASSWORD = "12341234";
|
||||
const DEFAULT_API_KEY = "3b1434980677dcf53fa8c4a611db3b1f0f88478790097515c0abb539102778b9"; // Some hash generated from password
|
||||
|
||||
|
||||
@@ -18,6 +18,17 @@ vi.mock("@homarr/db", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const DEFAULT_API_KEY = "8r45mfes43s3iw7x3oecto6dl9ilxnf9";
|
||||
const IMAGE_NAME = "linuxserver/sabnzbd:latest";
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -800,6 +800,9 @@ importers:
|
||||
'@homarr/common':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../common
|
||||
'@homarr/core':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../core
|
||||
'@homarr/db':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../db
|
||||
|
||||
Reference in New Issue
Block a user