refactor(logs): move to core package (#4586)
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import "@homarr/auth/env";
|
import "@homarr/auth/env";
|
||||||
import "@homarr/db/env";
|
import "@homarr/db/env";
|
||||||
import "@homarr/common/env";
|
import "@homarr/common/env";
|
||||||
import "@homarr/log/env";
|
import "@homarr/core/infrastructure/logs/env";
|
||||||
import "@homarr/docker/env";
|
import "@homarr/docker/env";
|
||||||
|
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/image-proxy": "workspace:^0.1.0",
|
"@homarr/image-proxy": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { IntegrationProvider } from "@homarr/auth/client";
|
|||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
||||||
import { isNullOrWhitespace } from "@homarr/common";
|
import { isNullOrWhitespace } from "@homarr/common";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { getI18n } from "@homarr/translation/server";
|
import { getI18n } from "@homarr/translation/server";
|
||||||
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
|
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ import type { Board, Item } from "../_types";
|
|||||||
import { DynamicClientBoard } from "./_dynamic-client";
|
import { DynamicClientBoard } from "./_dynamic-client";
|
||||||
import { BoardContentHeaderActions } from "./_header-actions";
|
import { BoardContentHeaderActions } from "./_header-actions";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "createBoardContentPage" });
|
||||||
|
|
||||||
export type Params = Record<string, unknown>;
|
export type Params = Record<string, unknown>;
|
||||||
|
|
||||||
interface Props<TParams extends Params> {
|
interface Props<TParams extends Params> {
|
||||||
@@ -57,7 +60,13 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
|
|||||||
|
|
||||||
for (const [kind, items] of itemsMap) {
|
for (const [kind, items] of itemsMap) {
|
||||||
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
|
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
|
||||||
logger.error(new Error("Failed to prefetch widget", { cause: error }));
|
logger.error(
|
||||||
|
new ErrorWithMetadata(
|
||||||
|
"Failed to prefetch widget",
|
||||||
|
{ widgetKind: kind, itemCount: items.length },
|
||||||
|
{ cause: error },
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { BoardProvider } from "@homarr/boards/context";
|
import { BoardProvider } from "@homarr/boards/context";
|
||||||
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { MainHeader } from "~/components/layout/header";
|
import { MainHeader } from "~/components/layout/header";
|
||||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||||
@@ -18,6 +18,8 @@ import { CustomCss } from "./(content)/_custom-css";
|
|||||||
import { BoardReadyProvider } from "./(content)/_ready-context";
|
import { BoardReadyProvider } from "./(content)/_ready-context";
|
||||||
import { BoardMantineProvider } from "./(content)/_theme";
|
import { BoardMantineProvider } from "./(content)/_theme";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "createBoardLayout" });
|
||||||
|
|
||||||
interface CreateBoardLayoutProps<TParams extends Params> {
|
interface CreateBoardLayoutProps<TParams extends Params> {
|
||||||
headerActions: JSX.Element;
|
headerActions: JSX.Element;
|
||||||
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { Select } from "@mantine/core";
|
import { Select } from "@mantine/core";
|
||||||
|
|
||||||
import type { LogLevel } from "@homarr/log/constants";
|
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
|
||||||
import { logLevelConfiguration, logLevels } from "@homarr/log/constants";
|
import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { useLogContext } from "./log-context";
|
import { useLogContext } from "./log-context";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { createContext, useContext, useMemo, useState } from "react";
|
import { createContext, useContext, useMemo, useState } from "react";
|
||||||
|
|
||||||
import type { LogLevel } from "@homarr/log/constants";
|
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
|
||||||
import { logLevels } from "@homarr/log/constants";
|
import { logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||||
|
|
||||||
const LogContext = createContext<{
|
const LogContext = createContext<{
|
||||||
level: LogLevel;
|
level: LogLevel;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "@xterm/xterm/css/xterm.css";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { env } from "@homarr/log/env";
|
import { logsEnv } from "@homarr/core/infrastructure/logs/env";
|
||||||
|
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
||||||
@@ -35,7 +35,7 @@ export default async function LogsManagementPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogContextProvider defaultLevel={env.LOG_LEVEL}>
|
<LogContextProvider defaultLevel={logsEnv.LEVEL}>
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
<DynamicBreadcrumb />
|
<DynamicBreadcrumb />
|
||||||
<LogLevelSelection />
|
<LogLevelSelection />
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { appRouter, createTRPCContext } from "@homarr/api";
|
|||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { hashPasswordAsync } from "@homarr/auth";
|
import { hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createSessionAsync } from "@homarr/auth/server";
|
import { createSessionAsync } from "@homarr/auth/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { apiKeys } from "@homarr/db/schema";
|
import { apiKeys } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
const logger = createLogger({ module: "trpcOpenApiRoute" });
|
||||||
|
|
||||||
const handlerAsync = async (req: NextRequest) => {
|
const handlerAsync = async (req: NextRequest) => {
|
||||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
||||||
@@ -27,7 +30,7 @@ const handlerAsync = async (req: NextRequest) => {
|
|||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -48,9 +51,10 @@ const getSessionOrDefaultFromHeadersAsync = async (
|
|||||||
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
||||||
|
|
||||||
if (!apiKeyId || !apiKey) {
|
if (!apiKeyId || !apiKey) {
|
||||||
logger.warn(
|
logger.warn("An attempt to authenticate over API has failed due to invalid API key format", {
|
||||||
`An attempt to authenticate over API has failed due to invalid API key format ip='${ipAdress}' userAgent='${userAgent}'`,
|
ipAdress,
|
||||||
);
|
userAgent,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,18 +78,21 @@ const getSessionOrDefaultFromHeadersAsync = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!apiKeyFromDb) {
|
if (!apiKeyFromDb) {
|
||||||
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||||
|
|
||||||
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||||
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Read session from API request and found user ${apiKeyFromDb.user.name} (${apiKeyFromDb.user.id})`);
|
logger.info("Read session from API request and found user", {
|
||||||
|
name: apiKeyFromDb.user.name,
|
||||||
|
id: apiKeyFromDb.user.id,
|
||||||
|
});
|
||||||
return await createSessionAsync(db, apiKeyFromDb.user);
|
return await createSessionAsync(db, apiKeyFromDb.user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
import { createHandlersAsync } from "@homarr/auth";
|
import { createHandlersAsync } from "@homarr/auth";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
const logger = createLogger({ module: "nextAuthRoute" });
|
||||||
|
|
||||||
export const GET = async (req: NextRequest) => {
|
export const GET = async (req: NextRequest) => {
|
||||||
const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req));
|
const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req));
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { performance } from "perf_hooks";
|
import { performance } from "perf_hooks";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { handshakeAsync } from "@homarr/redis";
|
import { handshakeAsync } from "@homarr/redis";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "healthLiveRoute" });
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const timeBeforeHealthCheck = performance.now();
|
const timeBeforeHealthCheck = performance.now();
|
||||||
const response = await executeAndAggregateAllHealthChecksAsync();
|
const response = await executeAndAggregateAllHealthChecksAsync();
|
||||||
logger.info(`Completed healthcheck after ${performance.now() - timeBeforeHealthCheck}ms`);
|
logger.info("Completed healthcheck", { elapsed: `${performance.now() - timeBeforeHealthCheck}ms` });
|
||||||
|
|
||||||
if (response.status === "healthy") {
|
if (response.status === "healthy") {
|
||||||
return new Response(JSON.stringify(response), {
|
return new Response(JSON.stringify(response), {
|
||||||
@@ -73,7 +76,7 @@ const executeHealthCheckSafelyAsync = async (
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
logger.error(`Healthcheck '${name}' has failed: ${error}`);
|
logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error }));
|
||||||
return {
|
return {
|
||||||
status: "unhealthy",
|
status: "unhealthy",
|
||||||
values: {
|
values: {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|||||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||||
import { trpcPath } from "@homarr/api/shared";
|
import { trpcPath } from "@homarr/api/shared";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "trpcRoute" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure basic CORS headers
|
* Configure basic CORS headers
|
||||||
@@ -31,7 +34,7 @@ const handler = auth(async (req) => {
|
|||||||
req,
|
req,
|
||||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import "server-only";
|
|||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "trpcCatchError" });
|
||||||
|
|
||||||
export const catchTrpcNotFound = (err: unknown) => {
|
export const catchTrpcNotFound = (err: unknown) => {
|
||||||
if (err instanceof TRPCError && err.code === "NOT_FOUND") {
|
if (err instanceof TRPCError && err.code === "NOT_FOUND") {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@homarr/analytics": "workspace:^0.1.0",
|
"@homarr/analytics": "workspace:^0.1.0",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^",
|
||||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
|
||||||
"@homarr/ping": "workspace:^0.1.0",
|
"@homarr/ping": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/request-handler": "workspace:^0.1.0",
|
"@homarr/request-handler": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { schedule, validate as validateCron } from "node-cron";
|
import { schedule, validate as validateCron } from "node-cron";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { IJobManager } from "@homarr/cron-job-api";
|
import type { IJobManager } from "@homarr/cron-job-api";
|
||||||
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
|
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
|
||||||
import type { Database, InferInsertModel } from "@homarr/db";
|
import type { Database, InferInsertModel } from "@homarr/db";
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { cronJobConfigurations } from "@homarr/db/schema";
|
import { cronJobConfigurations } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
const logger = createLogger({ module: "jobManager" });
|
||||||
|
|
||||||
export class JobManager implements IJobManager {
|
export class JobManager implements IJobManager {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,7 +25,7 @@ export class JobManager implements IJobManager {
|
|||||||
await this.jobGroup.stopAsync(name);
|
await this.jobGroup.stopAsync(name);
|
||||||
}
|
}
|
||||||
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
|
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
|
||||||
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
|
logger.info("Updating cron job interval", { name, expression: cron });
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
if (!validateCron(cron)) {
|
if (!validateCron(cron)) {
|
||||||
@@ -38,22 +40,22 @@ export class JobManager implements IJobManager {
|
|||||||
name,
|
name,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
logger.info(`Cron job interval updated name="${name}" expression="${cron}"`);
|
logger.info("Cron job interval updated", { name, expression: cron });
|
||||||
}
|
}
|
||||||
public async disableAsync(name: JobGroupKeys): Promise<void> {
|
public async disableAsync(name: JobGroupKeys): Promise<void> {
|
||||||
logger.info(`Disabling cron job name="${name}"`);
|
logger.info("Disabling cron job", { name });
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
|
|
||||||
await this.updateConfigurationAsync(name, { isEnabled: false });
|
await this.updateConfigurationAsync(name, { isEnabled: false });
|
||||||
await this.jobGroup.stopAsync(name);
|
await this.jobGroup.stopAsync(name);
|
||||||
logger.info(`Cron job disabled name="${name}"`);
|
logger.info("Cron job disabled", { name });
|
||||||
}
|
}
|
||||||
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
||||||
logger.info(`Enabling cron job name="${name}"`);
|
logger.info("Enabling cron job", { name });
|
||||||
await this.updateConfigurationAsync(name, { isEnabled: true });
|
await this.updateConfigurationAsync(name, { isEnabled: true });
|
||||||
await this.jobGroup.startAsync(name);
|
await this.jobGroup.startAsync(name);
|
||||||
logger.info(`Cron job enabled name="${name}"`);
|
logger.info("Cron job enabled", { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateConfigurationAsync(
|
private async updateConfigurationAsync(
|
||||||
@@ -64,9 +66,11 @@ export class JobManager implements IJobManager {
|
|||||||
where: (table, { eq }) => eq(table.name, name),
|
where: (table, { eq }) => eq(table.name, name),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(
|
logger.debug("Updating cron job configuration", {
|
||||||
`Updating cron job configuration name="${name}" configuration="${JSON.stringify(configuration)}" exists="${Boolean(existingConfig)}"`,
|
name,
|
||||||
);
|
configuration: JSON.stringify(configuration),
|
||||||
|
exists: Boolean(existingConfig),
|
||||||
|
});
|
||||||
|
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
await this.db
|
await this.db
|
||||||
@@ -74,7 +78,10 @@ export class JobManager implements IJobManager {
|
|||||||
// prevent updating the name, as it is the primary key
|
// prevent updating the name, as it is the primary key
|
||||||
.set({ ...configuration, name: undefined })
|
.set({ ...configuration, name: undefined })
|
||||||
.where(eq(cronJobConfigurations.name, name));
|
.where(eq(cronJobConfigurations.name, name));
|
||||||
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
|
logger.debug("Cron job configuration updated", {
|
||||||
|
name,
|
||||||
|
configuration: JSON.stringify(configuration),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +93,10 @@ export class JobManager implements IJobManager {
|
|||||||
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
||||||
isEnabled: configuration.isEnabled ?? true,
|
isEnabled: configuration.isEnabled ?? true,
|
||||||
});
|
});
|
||||||
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
|
logger.debug("Cron job configuration updated", {
|
||||||
|
name,
|
||||||
|
configuration: JSON.stringify(configuration),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAsync(): Promise<
|
public async getAllAsync(): Promise<
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
|
|||||||
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
||||||
import fastify from "fastify";
|
import fastify from "fastify";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import type { JobRouter } from "@homarr/cron-job-api";
|
import type { JobRouter } from "@homarr/cron-job-api";
|
||||||
import { jobRouter } from "@homarr/cron-job-api";
|
import { jobRouter } from "@homarr/cron-job-api";
|
||||||
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
|
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
|
||||||
import { jobGroup } from "@homarr/cron-jobs";
|
import { jobGroup } from "@homarr/cron-jobs";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { JobManager } from "./job-manager";
|
import { JobManager } from "./job-manager";
|
||||||
import { onStartAsync } from "./on-start";
|
import { onStartAsync } from "./on-start";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "tasksMain" });
|
||||||
|
|
||||||
const server = fastify({
|
const server = fastify({
|
||||||
maxParamLength: 5000,
|
maxParamLength: 5000,
|
||||||
});
|
});
|
||||||
@@ -27,7 +30,7 @@ server.register(fastifyTRPCPlugin, {
|
|||||||
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
|
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
|
||||||
}),
|
}),
|
||||||
onError({ path, error }) {
|
onError({ path, error }) {
|
||||||
logger.error(new Error(`Error in tasks tRPC handler path="${path}"`, { cause: error }));
|
logger.error(new ErrorWithMetadata("Error in tasks tRPC handler", { path }, { cause: error }));
|
||||||
},
|
},
|
||||||
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
|
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
|
||||||
});
|
});
|
||||||
@@ -39,9 +42,11 @@ void (async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await server.listen({ port: CRON_JOB_API_PORT });
|
await server.listen({ port: CRON_JOB_API_PORT });
|
||||||
logger.info(`Tasks web server started successfully port="${CRON_JOB_API_PORT}"`);
|
logger.info("Tasks web server started successfully", { port: CRON_JOB_API_PORT });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(new Error(`Failed to start tasks web server port="${CRON_JOB_API_PORT}"`, { cause: err }));
|
logger.error(
|
||||||
|
new ErrorWithMetadata("Failed to start tasks web server", { port: CRON_JOB_API_PORT }, { cause: err }),
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" });
|
const logger = createLogger({ module: "invalidateUpdateCheckerCache" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates the update checker cache on startup to ensure fresh data.
|
* Invalidates the update checker cache on startup to ensure fresh data.
|
||||||
@@ -11,8 +11,8 @@ export async function invalidateUpdateCheckerCacheAsync() {
|
|||||||
try {
|
try {
|
||||||
const handler = updateCheckerRequestHandler.handler({});
|
const handler = updateCheckerRequestHandler.handler({});
|
||||||
await handler.invalidateAsync();
|
await handler.invalidateAsync();
|
||||||
localLogger.debug("Update checker cache invalidated");
|
logger.debug("Update checker cache invalidated");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
logger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { env } from "@homarr/auth/env";
|
import { env } from "@homarr/auth/env";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { db, eq, inArray } from "@homarr/db";
|
import { db, eq, inArray } from "@homarr/db";
|
||||||
import { sessions, users } from "@homarr/db/schema";
|
import { sessions, users } from "@homarr/db/schema";
|
||||||
import { supportedAuthProviders } from "@homarr/definitions";
|
import { supportedAuthProviders } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "sessionCleanup" });
|
const logger = createLogger({ module: "sessionCleanup" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes sessions for users that have inactive auth providers.
|
* Deletes sessions for users that have inactive auth providers.
|
||||||
@@ -29,11 +29,13 @@ export async function cleanupSessionsAsync() {
|
|||||||
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
||||||
|
|
||||||
if (sessionsWithInactiveProviders.length > 0) {
|
if (sessionsWithInactiveProviders.length > 0) {
|
||||||
localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
|
logger.info("Deleted sessions for inactive providers", {
|
||||||
|
count: userIds.length,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
localLogger.debug("No sessions to delete");
|
logger.debug("No sessions to delete");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.error(new Error("Failed to clean up sessions", { cause: error }));
|
logger.error(new Error("Failed to clean up sessions", { cause: error }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { WebSocketServer } from "ws";
|
|||||||
import { appRouter, createTRPCContext } from "@homarr/api/websocket";
|
import { appRouter, createTRPCContext } from "@homarr/api/websocket";
|
||||||
import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth";
|
import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth";
|
||||||
import { parseCookies } from "@homarr/common";
|
import { parseCookies } from "@homarr/common";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
const logger = createLogger({ module: "websocketMain" });
|
||||||
|
|
||||||
const wss = new WebSocketServer({
|
const wss = new WebSocketServer({
|
||||||
port: 3001,
|
port: 3001,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "2.2.6"
|
"superjson": "2.2.6"
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { UmamiEventData } from "@umami/node";
|
import type { UmamiEventData } from "@umami/node";
|
||||||
import { Umami } from "@umami/node";
|
import { Umami } from "@umami/node";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { count, db } from "@homarr/db";
|
import { count, db } from "@homarr/db";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { integrations, items, users } from "@homarr/db/schema";
|
import { integrations, items, users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import type { defaultServerSettings } from "@homarr/server-settings";
|
import type { defaultServerSettings } from "@homarr/server-settings";
|
||||||
|
|
||||||
import { Stopwatch } from "../../common/src";
|
import { Stopwatch } from "../../common/src";
|
||||||
import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants";
|
import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "analytics" });
|
||||||
|
|
||||||
export const sendServerAnalyticsAsync = async () => {
|
export const sendServerAnalyticsAsync = async () => {
|
||||||
const stopWatch = new Stopwatch();
|
const stopWatch = new Stopwatch();
|
||||||
const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics");
|
const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics");
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"@homarr/docker": "workspace:^0.1.0",
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
|
||||||
"@homarr/old-import": "workspace:^0.1.0",
|
"@homarr/old-import": "workspace:^0.1.0",
|
||||||
"@homarr/old-schema": "workspace:^0.1.0",
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/ping": "workspace:^0.1.0",
|
"@homarr/ping": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { zfd } from "zod-form-data";
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
|
import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "certificateRouter" });
|
||||||
|
|
||||||
export const certificateRouter = createTRPCRouter({
|
export const certificateRouter = createTRPCRouter({
|
||||||
addCertificate: permissionRequiredProcedure
|
addCertificate: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import z from "zod/v4";
|
import z from "zod/v4";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
|
import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
|
||||||
import { cronJobApi } from "@homarr/cron-job-api/client";
|
import { cronJobApi } from "@homarr/cron-job-api/client";
|
||||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||||
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "cronJobsRouter" });
|
||||||
|
|
||||||
export const cronJobsRouter = createTRPCRouter({
|
export const cronJobsRouter = createTRPCRouter({
|
||||||
triggerJob: permissionRequiredProcedure
|
triggerJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
import { createId, objectEntries } from "@homarr/common";
|
import { createId, objectEntries } from "@homarr/common";
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, asc, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
import { and, asc, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +27,6 @@ import {
|
|||||||
integrationSecretKindObject,
|
integrationSecretKindObject,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
import {
|
import {
|
||||||
integrationCreateSchema,
|
integrationCreateSchema,
|
||||||
@@ -40,6 +40,8 @@ import { throwIfActionForbiddenAsync } from "./integration-access";
|
|||||||
import { MissingSecretError, testConnectionAsync } from "./integration-test-connection";
|
import { MissingSecretError, testConnectionAsync } from "./integration-test-connection";
|
||||||
import { mapTestConnectionError } from "./map-test-connection-error";
|
import { mapTestConnectionError } from "./map-test-connection-error";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "integrationRouter" });
|
||||||
|
|
||||||
export const integrationRouter = createTRPCRouter({
|
export const integrationRouter = createTRPCRouter({
|
||||||
all: publicProcedure.query(async ({ ctx }) => {
|
all: publicProcedure.query(async ({ ctx }) => {
|
||||||
const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
|
const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { decryptSecret } from "@homarr/common/server";
|
import { decryptSecret } from "@homarr/common/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
const logger = createLogger({ module: "integrationTestConnection" });
|
||||||
|
|
||||||
type FormIntegration = Omit<Integration, "appId"> & {
|
type FormIntegration = Omit<Integration, "appId"> & {
|
||||||
secrets: {
|
secrets: {
|
||||||
@@ -35,8 +38,13 @@ export const testConnectionAsync = async (
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
new Error(
|
new ErrorWithMetadata(
|
||||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
|
"Failed to decrypt secret from database",
|
||||||
|
{
|
||||||
|
integrationName: integration.name,
|
||||||
|
integrationKind: integration.kind,
|
||||||
|
secretKind: secret.kind,
|
||||||
|
},
|
||||||
{ cause: error },
|
{ cause: error },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { V1NodeList, VersionInfo } from "@kubernetes/client-node";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { ClusterResourceCount, KubernetesCluster } from "@homarr/definitions";
|
import type { ClusterResourceCount, KubernetesCluster } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -129,7 +128,6 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve cluster", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes cluster",
|
message: "An error occurred while fetching Kubernetes cluster",
|
||||||
@@ -165,7 +163,6 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
{ label: "volumes", count: volumes.items.length },
|
{ label: "volumes", count: volumes.items.length },
|
||||||
];
|
];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve cluster resource counts", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes resources count",
|
message: "An error occurred while fetching Kubernetes resources count",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesBaseResource } from "@homarr/definitions";
|
import type { KubernetesBaseResource } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -25,7 +24,6 @@ export const configMapsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve configMaps", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes ConfigMaps",
|
message: "An error occurred while fetching Kubernetes ConfigMaps",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { V1HTTPIngressPath, V1Ingress, V1IngressRule } from "@kubernetes/cl
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesIngress, KubernetesIngressPath, KubernetesIngressRuleAndPath } from "@homarr/definitions";
|
import type { KubernetesIngress, KubernetesIngressPath, KubernetesIngressRuleAndPath } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -43,7 +42,6 @@ export const ingressesRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return ingresses.items.map(mapIngress);
|
return ingresses.items.map(mapIngress);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve ingresses", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes ingresses",
|
message: "An error occurred while fetching Kubernetes ingresses",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesNamespace, KubernetesNamespaceState } from "@homarr/definitions";
|
import type { KubernetesNamespace, KubernetesNamespaceState } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -25,7 +24,6 @@ export const namespacesRouter = createTRPCRouter({
|
|||||||
} satisfies KubernetesNamespace;
|
} satisfies KubernetesNamespace;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve namespaces", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes namespaces",
|
message: "An error occurred while fetching Kubernetes namespaces",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesNode, KubernetesNodeState } from "@homarr/definitions";
|
import type { KubernetesNode, KubernetesNodeState } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -57,7 +56,6 @@ export const nodesRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve nodes", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes nodes",
|
message: "An error occurred while fetching Kubernetes nodes",
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import type { KubeConfig, V1OwnerReference } from "@kubernetes/client-node";
|
|||||||
import { AppsV1Api } from "@kubernetes/client-node";
|
import { AppsV1Api } from "@kubernetes/client-node";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { KubernetesPod } from "@homarr/definitions";
|
import type { KubernetesPod } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
import { KubernetesClient } from "../kubernetes-client";
|
import { KubernetesClient } from "../kubernetes-client";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "podsRouter" });
|
||||||
|
|
||||||
export const podsRouter = createTRPCRouter({
|
export const podsRouter = createTRPCRouter({
|
||||||
getPods: permissionRequiredProcedure
|
getPods: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
@@ -55,7 +57,6 @@ export const podsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return pods;
|
return pods;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve pods", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes pods",
|
message: "An error occurred while fetching Kubernetes pods",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesSecret } from "@homarr/definitions";
|
import type { KubernetesSecret } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -25,7 +24,6 @@ export const secretsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve secrets", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes secrets",
|
message: "An error occurred while fetching Kubernetes secrets",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesService } from "@homarr/definitions";
|
import type { KubernetesService } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -29,7 +28,6 @@ export const servicesRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve services", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes services",
|
message: "An error occurred while fetching Kubernetes services",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { KubernetesVolume } from "@homarr/definitions";
|
import type { KubernetesVolume } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||||
@@ -31,7 +30,6 @@ export const volumesRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Unable to retrieve volumes", error);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while fetching Kubernetes Volumes",
|
message: "An error occurred while fetching Kubernetes Volumes",
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import z from "zod/v4";
|
import z from "zod/v4";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { logLevels } from "@homarr/log/constants";
|
import { logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||||
import type { LoggerMessage } from "@homarr/redis";
|
import type { LoggerMessage } from "@homarr/redis";
|
||||||
import { loggingChannel } from "@homarr/redis";
|
import { loggingChannel } from "@homarr/redis";
|
||||||
import { zodEnumFromArray } from "@homarr/validation/enums";
|
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "logRouter" });
|
||||||
|
|
||||||
export const logRouter = createTRPCRouter({
|
export const logRouter = createTRPCRouter({
|
||||||
subscribe: permissionRequiredProcedure
|
subscribe: permissionRequiredProcedure
|
||||||
.requiresPermission("other-view-logs")
|
.requiresPermission("other-view-logs")
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { asc, eq, like } from "@homarr/db";
|
import { asc, eq, like } from "@homarr/db";
|
||||||
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { searchEngines, users } from "@homarr/db/schema";
|
import { searchEngines, users } from "@homarr/db/schema";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||||
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||||
@@ -14,6 +14,8 @@ import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/va
|
|||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "searchEngineRouter" });
|
||||||
|
|
||||||
export const searchEngineRouter = createTRPCRouter({
|
export const searchEngineRouter = createTRPCRouter({
|
||||||
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
|
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
|
||||||
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "updateCheckerRouter" });
|
||||||
|
|
||||||
export const updateCheckerRouter = createTRPCRouter({
|
export const updateCheckerRouter = createTRPCRouter({
|
||||||
getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, eq, like } from "@homarr/db";
|
import { and, eq, like } from "@homarr/db";
|
||||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||||
@@ -10,7 +11,6 @@ import { boards, groupMembers, groupPermissions, groups, invites, users } from "
|
|||||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +39,8 @@ import { throwIfCredentialsDisabled } from "./invite/checks";
|
|||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
|
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "userRouter" });
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
initUser: onboardingProcedure
|
initUser: onboardingProcedure
|
||||||
.requiresStep("user")
|
.requiresStep("user")
|
||||||
@@ -364,9 +366,11 @@ export const userRouter = createTRPCRouter({
|
|||||||
// Admins can change the password of other users without providing the previous password
|
// Admins can change the password of other users without providing the previous password
|
||||||
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
|
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
|
||||||
|
|
||||||
logger.info(
|
logger.info("Changing user password", {
|
||||||
`User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`,
|
actorId: ctx.session.user.id,
|
||||||
);
|
targetUserId: input.userId,
|
||||||
|
previousPasswordRequired: isPreviousPasswordRequired,
|
||||||
|
});
|
||||||
|
|
||||||
if (isPreviousPasswordRequired) {
|
if (isPreviousPasswordRequired) {
|
||||||
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { Indexer } from "@homarr/integrations/types";
|
import type { Indexer } from "@homarr/integrations/types";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
|
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
@@ -61,10 +60,10 @@ export const indexerManagerRouter = createTRPCRouter({
|
|||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const client = await createIntegrationAsync(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
await client.testAllAsync().catch((err) => {
|
await client.testAllAsync().catch((err) => {
|
||||||
logger.error("indexer-manager router - ", err);
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: `Failed to test all indexers for ${integration.name} (${integration.id})`,
|
message: `Failed to test all indexers for ${integration.name} (${integration.id})`,
|
||||||
|
cause: err,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import { ZodError } from "zod/v4";
|
|||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { FlattenError } from "@homarr/common";
|
import { FlattenError } from "@homarr/common";
|
||||||
import { userAgent } from "@homarr/common/server";
|
import { userAgent } from "@homarr/common/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
|
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "trpc" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
*
|
*
|
||||||
@@ -36,7 +38,7 @@ export const createTRPCContext = (opts: { headers: Headers; session: Session | n
|
|||||||
const session = opts.session;
|
const session = opts.session;
|
||||||
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
||||||
|
|
||||||
logger.info(`tRPC request from ${source} by user '${session?.user.name} (${session?.user.id})'`, session?.user);
|
logger.info("Received tRPC request", { source, userId: session?.user.id, userName: session?.user.name });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { cookies } from "next/headers";
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { createAdapter } from "./adapter";
|
import { createAdapter } from "./adapter";
|
||||||
import { createSessionCallback } from "./callbacks";
|
import { createSessionCallback } from "./callbacks";
|
||||||
@@ -18,6 +18,8 @@ import { OidcProvider } from "./providers/oidc/oidc-provider";
|
|||||||
import { createRedirectUri } from "./redirect";
|
import { createRedirectUri } from "./redirect";
|
||||||
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
|
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "authConfiguration" });
|
||||||
|
|
||||||
// See why it's unknown in the [...nextauth]/route.ts file
|
// See why it's unknown in the [...nextauth]/route.ts file
|
||||||
export const createConfiguration = (
|
export const createConfiguration = (
|
||||||
provider: SupportedAuthProvider | "unknown",
|
provider: SupportedAuthProvider | "unknown",
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import { cookies } from "next/headers";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import type { NextAuthConfig } from "next-auth";
|
import type { NextAuthConfig } from "next-auth";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { and, eq, inArray } from "@homarr/db";
|
import { and, eq, inArray } from "@homarr/db";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { groupMembers, groups, users } from "@homarr/db/schema";
|
import { groupMembers, groups, users } from "@homarr/db/schema";
|
||||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "authEvents" });
|
||||||
|
|
||||||
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
||||||
return async ({ user, profile }) => {
|
return async ({ user, profile }) => {
|
||||||
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
|
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
|
||||||
@@ -43,9 +45,11 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
|
|
||||||
if (dbUser.name !== user.name) {
|
if (dbUser.name !== user.name) {
|
||||||
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
||||||
logger.info(
|
logger.info("Username for user of credentials provider has changed.", {
|
||||||
`Username for user of credentials provider has changed. user=${user.id} old=${dbUser.name} new=${user.name}`,
|
userId: user.id,
|
||||||
);
|
oldName: dbUser.name,
|
||||||
|
newName: user.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
@@ -56,9 +60,11 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
|
|
||||||
if (dbUser.name !== profileUsername) {
|
if (dbUser.name !== profileUsername) {
|
||||||
await db.update(users).set({ name: profileUsername }).where(eq(users.id, user.id));
|
await db.update(users).set({ name: profileUsername }).where(eq(users.id, user.id));
|
||||||
logger.info(
|
logger.info("Username for user of oidc provider has changed.", {
|
||||||
`Username for user of oidc provider has changed. user=${user.id} old='${dbUser.name}' new='${profileUsername}'`,
|
userId: user.id,
|
||||||
);
|
oldName: dbUser.name,
|
||||||
|
newName: profileUsername,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -67,11 +73,13 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
!dbUser.image?.startsWith("data:")
|
!dbUser.image?.startsWith("data:")
|
||||||
) {
|
) {
|
||||||
await db.update(users).set({ image: profile.picture }).where(eq(users.id, user.id));
|
await db.update(users).set({ image: profile.picture }).where(eq(users.id, user.id));
|
||||||
logger.info(`Profile picture for user of oidc provider has changed. user=${user.id}'`);
|
logger.info("Profile picture for user of oidc provider has changed.", {
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
|
logger.info("User logged in", { userId: user.id, userName: dbUser.name, timestamp: dayjs().format() });
|
||||||
|
|
||||||
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
|
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
|
||||||
(await cookies()).set(colorSchemeCookieKey, dbUser.colorScheme, {
|
(await cookies()).set(colorSchemeCookieKey, dbUser.colorScheme, {
|
||||||
@@ -96,7 +104,7 @@ const addUserToEveryoneGroupIfNotMemberAsync = async (db: Database, userId: stri
|
|||||||
userId,
|
userId,
|
||||||
groupId: dbEveryoneGroup.id,
|
groupId: dbEveryoneGroup.id,
|
||||||
});
|
});
|
||||||
logger.info(`Added user to everyone group. user=${userId}`);
|
logger.info("Added user to everyone group.", { userId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,9 +126,10 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (missingExternalGroupsForUser.length > 0) {
|
if (missingExternalGroupsForUser.length > 0) {
|
||||||
logger.debug(
|
logger.debug("Homarr does not have the user in certain groups.", {
|
||||||
`Homarr does not have the user in certain groups. user=${userId} count=${missingExternalGroupsForUser.length}`,
|
user: userId,
|
||||||
);
|
count: missingExternalGroupsForUser.length,
|
||||||
|
});
|
||||||
|
|
||||||
const groupIds = await db.query.groups.findMany({
|
const groupIds = await db.query.groups.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
@@ -129,7 +138,10 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
where: inArray(groups.name, missingExternalGroupsForUser),
|
where: inArray(groups.name, missingExternalGroupsForUser),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`Homarr has found groups in the database user is not in. user=${userId} count=${groupIds.length}`);
|
logger.debug("Homarr has found groups in the database user is not in.", {
|
||||||
|
user: userId,
|
||||||
|
count: groupIds.length,
|
||||||
|
});
|
||||||
|
|
||||||
if (groupIds.length > 0) {
|
if (groupIds.length > 0) {
|
||||||
await db.insert(groupMembers).values(
|
await db.insert(groupMembers).values(
|
||||||
@@ -139,9 +151,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Added user to groups successfully. user=${userId} count=${groupIds.length}`);
|
logger.info("Added user to groups successfully.", { user: userId, count: groupIds.length });
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`User is already in all groups of Homarr. user=${userId}`);
|
logger.debug("User is already in all groups of Homarr.", { user: userId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,9 +166,10 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
||||||
logger.debug(
|
logger.debug("Homarr has the user in certain groups that LDAP does not have.", {
|
||||||
`Homarr has the user in certain groups that LDAP does not have. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
user: userId,
|
||||||
);
|
count: groupsUserIsNoLongerMemberOfExternally.length,
|
||||||
|
});
|
||||||
|
|
||||||
await db.delete(groupMembers).where(
|
await db.delete(groupMembers).where(
|
||||||
and(
|
and(
|
||||||
@@ -168,8 +181,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info("Removed user from groups successfully.", {
|
||||||
`Removed user from groups successfully. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
user: userId,
|
||||||
);
|
count: groupsUserIsNoLongerMemberOfExternally.length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import type { userSignInSchema } from "@homarr/validation/user";
|
import type { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "basicAuthorization" });
|
||||||
|
|
||||||
export const authorizeWithBasicCredentialsAsync = async (
|
export const authorizeWithBasicCredentialsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
credentials: z.infer<typeof userSignInSchema>,
|
credentials: z.infer<typeof userSignInSchema>,
|
||||||
@@ -16,19 +18,19 @@ export const authorizeWithBasicCredentialsAsync = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.password) {
|
if (!user?.password) {
|
||||||
logger.info(`user ${credentials.name} was not found`);
|
logger.info("User not found", { userName: credentials.name });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`user ${user.name} is trying to log in. checking password...`);
|
logger.info("User is trying to log in. Checking password...", { userName: user.name });
|
||||||
const isValidPassword = await bcrypt.compare(credentials.password, user.password);
|
const isValidPassword = await bcrypt.compare(credentials.password, user.password);
|
||||||
|
|
||||||
if (!isValidPassword) {
|
if (!isValidPassword) {
|
||||||
logger.warn(`password for user ${user.name} was incorrect`);
|
logger.warn("Password for user was incorrect", { userName: user.name });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`user ${user.name} successfully authorized`);
|
logger.info("User successfully authorized", { userName: user.name });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { CredentialsSignin } from "@auth/core/errors";
|
import { CredentialsSignin } from "@auth/core/errors";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId, extractErrorMessage } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { Database, InferInsertModel } from "@homarr/db";
|
import type { Database, InferInsertModel } from "@homarr/db";
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import type { ldapSignInSchema } from "@homarr/validation/user";
|
import type { ldapSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { env } from "../../../env";
|
import { env } from "../../../env";
|
||||||
import { LdapClient } from "../ldap-client";
|
import { LdapClient } from "../ldap-client";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "ldapAuthorization" });
|
||||||
|
|
||||||
export const authorizeWithLdapCredentialsAsync = async (
|
export const authorizeWithLdapCredentialsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
credentials: z.infer<typeof ldapSignInSchema>,
|
credentials: z.infer<typeof ldapSignInSchema>,
|
||||||
) => {
|
) => {
|
||||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
logger.info("User is trying to log in using LDAP. Connecting to LDAP server...", { userName: credentials.name });
|
||||||
const client = new LdapClient();
|
const client = new LdapClient();
|
||||||
await client
|
await client
|
||||||
.bindAsync({
|
.bindAsync({
|
||||||
@@ -23,8 +25,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
password: env.AUTH_LDAP_BIND_PASSWORD,
|
password: env.AUTH_LDAP_BIND_PASSWORD,
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error(`Failed to connect to LDAP server ${extractErrorMessage(error)}`);
|
throw new CredentialsSignin("Failed to connect to LDAP server", { cause: error });
|
||||||
throw new CredentialsSignin();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("Connected to LDAP server. Searching for user...");
|
logger.info("Connected to LDAP server. Searching for user...");
|
||||||
@@ -48,21 +49,21 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!ldapUser) {
|
if (!ldapUser) {
|
||||||
logger.warn(`User ${credentials.name} not found in LDAP`);
|
throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`);
|
||||||
throw new CredentialsSignin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email
|
// Validate email
|
||||||
const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
|
const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
|
||||||
|
|
||||||
if (!mailResult.success) {
|
if (!mailResult.success) {
|
||||||
logger.error(
|
logger.error("User found in LDAP but with invalid or non-existing Email", {
|
||||||
`User ${credentials.name} found but with invalid or non-existing Email. Not Supported: "${ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]}"`,
|
userName: credentials.name,
|
||||||
);
|
emailValue: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
||||||
throw new CredentialsSignin();
|
});
|
||||||
|
throw new CredentialsSignin("User found in LDAP but with invalid or non-existing Email");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`User ${credentials.name} found in LDAP. Logging in...`);
|
logger.info("User found in LDAP. Logging in...", { userName: credentials.name });
|
||||||
|
|
||||||
// Bind with user credentials to check if the password is correct
|
// Bind with user credentials to check if the password is correct
|
||||||
const userClient = new LdapClient();
|
const userClient = new LdapClient();
|
||||||
@@ -72,12 +73,12 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
password: credentials.password,
|
password: credentials.password,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
logger.warn(`Wrong credentials for user ${credentials.name}`);
|
logger.warn("Wrong credentials for user", { userName: credentials.name });
|
||||||
throw new CredentialsSignin();
|
throw new CredentialsSignin();
|
||||||
});
|
});
|
||||||
await userClient.disconnectAsync();
|
await userClient.disconnectAsync();
|
||||||
|
|
||||||
logger.info(`User ${credentials.name} logged in successfully, retrieving user groups...`);
|
logger.info("User credentials are correct. Retrieving user groups...", { userName: credentials.name });
|
||||||
|
|
||||||
const userGroups = await client
|
const userGroups = await client
|
||||||
.searchAsync({
|
.searchAsync({
|
||||||
@@ -93,7 +94,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
})
|
})
|
||||||
.then((entries) => entries.map((entry) => entry.cn).filter((group): group is string => group !== undefined));
|
.then((entries) => entries.map((entry) => entry.cn).filter((group): group is string => group !== undefined));
|
||||||
|
|
||||||
logger.info(`Found ${userGroups.length} groups for user ${credentials.name}.`);
|
logger.info("User groups retrieved", { userName: credentials.name, groups: userGroups.length });
|
||||||
|
|
||||||
await client.disconnectAsync();
|
await client.disconnectAsync();
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.info(`User ${credentials.name} not found in the database. Creating...`);
|
logger.info("User not found in the database. Creating...", { userName: credentials.name });
|
||||||
|
|
||||||
const insertUser = {
|
const insertUser = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -126,7 +127,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
|||||||
|
|
||||||
user = insertUser;
|
user = insertUser;
|
||||||
|
|
||||||
logger.info(`User ${credentials.name} created successfully.`);
|
logger.info("User created successfully", { userName: credentials.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dns-caching": "^0.2.9",
|
"dns-caching": "^0.2.9",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DnsCacheManager } from "dns-caching";
|
import { DnsCacheManager } from "dns-caching";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { env } from "../env";
|
import { env } from "../env";
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@ declare global {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "dns" });
|
||||||
|
|
||||||
// Initialize global.homarr if not present
|
// Initialize global.homarr if not present
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
global.homarr ??= {};
|
global.homarr ??= {};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import type { AnyRequestError } from "../request-error";
|
import type { AnyRequestError } from "../request-error";
|
||||||
import { RequestError } from "../request-error";
|
import { RequestError } from "../request-error";
|
||||||
import { ResponseError } from "../response-error";
|
import { ResponseError } from "../response-error";
|
||||||
@@ -9,11 +7,15 @@ import { matchErrorCode } from "./fetch-http-error-handler";
|
|||||||
import { HttpErrorHandler } from "./http-error-handler";
|
import { HttpErrorHandler } from "./http-error-handler";
|
||||||
|
|
||||||
export class AxiosHttpErrorHandler extends HttpErrorHandler {
|
export class AxiosHttpErrorHandler extends HttpErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("axios");
|
||||||
|
}
|
||||||
|
|
||||||
handleRequestError(error: unknown): AnyRequestError | undefined {
|
handleRequestError(error: unknown): AnyRequestError | undefined {
|
||||||
if (!(error instanceof AxiosError)) return undefined;
|
if (!(error instanceof AxiosError)) return undefined;
|
||||||
if (error.code === undefined) return undefined;
|
if (error.code === undefined) return undefined;
|
||||||
|
|
||||||
logger.debug("Received Axios request error", {
|
this.logRequestError({
|
||||||
code: error.code,
|
code: error.code,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
});
|
});
|
||||||
@@ -28,8 +30,7 @@ export class AxiosHttpErrorHandler extends HttpErrorHandler {
|
|||||||
handleResponseError(error: unknown): ResponseError | undefined {
|
handleResponseError(error: unknown): ResponseError | undefined {
|
||||||
if (!(error instanceof AxiosError)) return undefined;
|
if (!(error instanceof AxiosError)) return undefined;
|
||||||
if (error.response === undefined) return undefined;
|
if (error.response === undefined) return undefined;
|
||||||
|
this.logResponseError({
|
||||||
logger.debug("Received Axios response error", {
|
|
||||||
status: error.response.status,
|
status: error.response.status,
|
||||||
url: error.response.config.url,
|
url: error.response.config.url,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { objectEntries } from "../../../object";
|
import { objectEntries } from "../../../object";
|
||||||
import type { Modify } from "../../../types";
|
import type { Modify } from "../../../types";
|
||||||
import type { AnyRequestError, AnyRequestErrorInput, RequestErrorCode, RequestErrorReason } from "../request-error";
|
import type { AnyRequestError, AnyRequestErrorInput, RequestErrorCode, RequestErrorReason } from "../request-error";
|
||||||
@@ -9,13 +7,13 @@ import { HttpErrorHandler } from "./http-error-handler";
|
|||||||
|
|
||||||
export class FetchHttpErrorHandler extends HttpErrorHandler {
|
export class FetchHttpErrorHandler extends HttpErrorHandler {
|
||||||
constructor(private type = "undici") {
|
constructor(private type = "undici") {
|
||||||
super();
|
super(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequestError(error: unknown): AnyRequestError | undefined {
|
handleRequestError(error: unknown): AnyRequestError | undefined {
|
||||||
if (!isTypeErrorWithCode(error)) return undefined;
|
if (!isTypeErrorWithCode(error)) return undefined;
|
||||||
|
|
||||||
logger.debug(`Received ${this.type} request error`, {
|
this.logRequestError({
|
||||||
code: error.cause.code,
|
code: error.cause.code,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
|
import type { Logger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { AnyRequestError } from "../request-error";
|
import type { AnyRequestError } from "../request-error";
|
||||||
import type { ResponseError } from "../response-error";
|
import type { ResponseError } from "../response-error";
|
||||||
|
|
||||||
export abstract class HttpErrorHandler {
|
export abstract class HttpErrorHandler {
|
||||||
|
protected logger: Logger;
|
||||||
|
|
||||||
|
constructor(type: string) {
|
||||||
|
this.logger = createLogger({ module: "httpErrorHandler", type });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logRequestError<T extends { code: string }>(metadata: T) {
|
||||||
|
this.logger.debug("Received request error", metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logResponseError<T extends { status: number; url: string | undefined }>(metadata: T) {
|
||||||
|
this.logger.debug("Received response error", metadata);
|
||||||
|
}
|
||||||
|
|
||||||
abstract handleRequestError(error: unknown): AnyRequestError | undefined;
|
abstract handleRequestError(error: unknown): AnyRequestError | undefined;
|
||||||
abstract handleResponseError(error: unknown): ResponseError | undefined;
|
abstract handleResponseError(error: unknown): ResponseError | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { FetchError } from "node-fetch";
|
import { FetchError } from "node-fetch";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { RequestError } from "../request-error";
|
import { RequestError } from "../request-error";
|
||||||
import type { AnyRequestError } from "../request-error";
|
import type { AnyRequestError } from "../request-error";
|
||||||
import type { ResponseError } from "../response-error";
|
import type { ResponseError } from "../response-error";
|
||||||
@@ -15,14 +13,14 @@ import { HttpErrorHandler } from "./http-error-handler";
|
|||||||
*/
|
*/
|
||||||
export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
|
export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
|
||||||
constructor(private type = "node-fetch") {
|
constructor(private type = "node-fetch") {
|
||||||
super();
|
super(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequestError(error: unknown): AnyRequestError | undefined {
|
handleRequestError(error: unknown): AnyRequestError | undefined {
|
||||||
if (!(error instanceof FetchError)) return undefined;
|
if (!(error instanceof FetchError)) return undefined;
|
||||||
if (error.code === undefined) return undefined;
|
if (error.code === undefined) return undefined;
|
||||||
|
|
||||||
logger.debug(`Received ${this.type} request error`, {
|
this.logRequestError({
|
||||||
code: error.code,
|
code: error.code,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { ResponseError } from "../response-error";
|
|||||||
import { HttpErrorHandler } from "./http-error-handler";
|
import { HttpErrorHandler } from "./http-error-handler";
|
||||||
|
|
||||||
export class OctokitHttpErrorHandler extends HttpErrorHandler {
|
export class OctokitHttpErrorHandler extends HttpErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("octokit");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I wasn't able to get a request error triggered. Therefore we ignore them for now
|
* I wasn't able to get a request error triggered. Therefore we ignore them for now
|
||||||
* and just forward them as unknown errors
|
* and just forward them as unknown errors
|
||||||
@@ -16,6 +20,11 @@ export class OctokitHttpErrorHandler extends HttpErrorHandler {
|
|||||||
handleResponseError(error: unknown): ResponseError | undefined {
|
handleResponseError(error: unknown): ResponseError | undefined {
|
||||||
if (!(error instanceof OctokitRequestError)) return undefined;
|
if (!(error instanceof OctokitRequestError)) return undefined;
|
||||||
|
|
||||||
|
this.logResponseError({
|
||||||
|
status: error.status,
|
||||||
|
url: error.response?.url,
|
||||||
|
});
|
||||||
|
|
||||||
return new ResponseError({
|
return new ResponseError({
|
||||||
status: error.status,
|
status: error.status,
|
||||||
url: error.response?.url,
|
url: error.response?.url,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { FetchError } from "ofetch";
|
import { FetchError } from "ofetch";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import type { AnyRequestError } from "../request-error";
|
import type { AnyRequestError } from "../request-error";
|
||||||
import { ResponseError } from "../response-error";
|
import { ResponseError } from "../response-error";
|
||||||
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
|
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
|
||||||
@@ -14,6 +12,10 @@ import { HttpErrorHandler } from "./http-error-handler";
|
|||||||
* It is for example used within the ctrl packages like qbittorrent, deluge, transmission, etc.
|
* It is for example used within the ctrl packages like qbittorrent, deluge, transmission, etc.
|
||||||
*/
|
*/
|
||||||
export class OFetchHttpErrorHandler extends HttpErrorHandler {
|
export class OFetchHttpErrorHandler extends HttpErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("ofetch");
|
||||||
|
}
|
||||||
|
|
||||||
handleRequestError(error: unknown): AnyRequestError | undefined {
|
handleRequestError(error: unknown): AnyRequestError | undefined {
|
||||||
if (!(error instanceof FetchError)) return undefined;
|
if (!(error instanceof FetchError)) return undefined;
|
||||||
if (!(error.cause instanceof TypeError)) return undefined;
|
if (!(error.cause instanceof TypeError)) return undefined;
|
||||||
@@ -28,7 +30,7 @@ export class OFetchHttpErrorHandler extends HttpErrorHandler {
|
|||||||
if (!(error instanceof FetchError)) return undefined;
|
if (!(error instanceof FetchError)) return undefined;
|
||||||
if (error.response === undefined) return undefined;
|
if (error.response === undefined) return undefined;
|
||||||
|
|
||||||
logger.debug("Received ofetch response error", {
|
this.logResponseError({
|
||||||
status: error.response.status,
|
status: error.response.status,
|
||||||
url: error.response.url,
|
url: error.response.url,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import type { AnyRequestError } from "../request-error";
|
import type { AnyRequestError } from "../request-error";
|
||||||
import { ResponseError } from "../response-error";
|
import { ResponseError } from "../response-error";
|
||||||
import { HttpErrorHandler } from "./http-error-handler";
|
import { HttpErrorHandler } from "./http-error-handler";
|
||||||
import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler";
|
import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler";
|
||||||
|
|
||||||
export class TsdavHttpErrorHandler extends HttpErrorHandler {
|
export class TsdavHttpErrorHandler extends HttpErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("tsdav");
|
||||||
|
}
|
||||||
|
|
||||||
handleRequestError(error: unknown): AnyRequestError | undefined {
|
handleRequestError(error: unknown): AnyRequestError | undefined {
|
||||||
return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error);
|
return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error);
|
||||||
}
|
}
|
||||||
@@ -16,8 +18,9 @@ export class TsdavHttpErrorHandler extends HttpErrorHandler {
|
|||||||
// https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86
|
// https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86
|
||||||
if (error.message !== "Invalid credentials") return undefined;
|
if (error.message !== "Invalid credentials") return undefined;
|
||||||
|
|
||||||
logger.debug("Received tsdav response error", {
|
this.logResponseError({
|
||||||
status: 401,
|
status: 401,
|
||||||
|
url: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ResponseError({ status: 401, url: "?" });
|
return new ResponseError({ status: 401, url: "?" });
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { ParseError } from "../parse-error";
|
import { ParseError } from "../parse-error";
|
||||||
import { ParseErrorHandler } from "./parse-error-handler";
|
import { ParseErrorHandler } from "./parse-error-handler";
|
||||||
|
|
||||||
export class JsonParseErrorHandler extends ParseErrorHandler {
|
export class JsonParseErrorHandler extends ParseErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("json");
|
||||||
|
}
|
||||||
|
|
||||||
handleParseError(error: unknown): ParseError | undefined {
|
handleParseError(error: unknown): ParseError | undefined {
|
||||||
if (!(error instanceof SyntaxError)) return undefined;
|
if (!(error instanceof SyntaxError)) return undefined;
|
||||||
|
|
||||||
logger.debug("Received JSON parse error", {
|
this.logParseError({
|
||||||
message: error.message,
|
message: error.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
import type { Logger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { ParseError } from "../parse-error";
|
import type { ParseError } from "../parse-error";
|
||||||
|
|
||||||
export abstract class ParseErrorHandler {
|
export abstract class ParseErrorHandler {
|
||||||
|
protected logger: Logger;
|
||||||
|
constructor(type: string) {
|
||||||
|
this.logger = createLogger({ module: "parseErrorHandler", type });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logParseError(metadata?: Record<string, unknown>) {
|
||||||
|
this.logger.debug("Received parse error", metadata);
|
||||||
|
}
|
||||||
|
|
||||||
abstract handleParseError(error: unknown): ParseError | undefined;
|
abstract handleParseError(error: unknown): ParseError | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { ZodError } from "zod/v4";
|
import { ZodError } from "zod/v4";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { ParseError } from "../parse-error";
|
import { ParseError } from "../parse-error";
|
||||||
import { ParseErrorHandler } from "./parse-error-handler";
|
import { ParseErrorHandler } from "./parse-error-handler";
|
||||||
|
|
||||||
export class ZodParseErrorHandler extends ParseErrorHandler {
|
export class ZodParseErrorHandler extends ParseErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
super("zod");
|
||||||
|
}
|
||||||
|
|
||||||
handleParseError(error: unknown): ParseError | undefined {
|
handleParseError(error: unknown): ParseError | undefined {
|
||||||
if (!(error instanceof ZodError)) return undefined;
|
if (!(error instanceof ZodError)) return undefined;
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export class ZodParseErrorHandler extends ParseErrorHandler {
|
|||||||
prefix: null,
|
prefix: null,
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
logger.debug("Received Zod parse error");
|
this.logParseError();
|
||||||
|
|
||||||
return new ParseError(message, { cause: error });
|
return new ParseError(message, { cause: error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Dispatcher } from "undici";
|
import type { Dispatcher } from "undici";
|
||||||
import { Agent } from "undici";
|
import { Agent } from "undici";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
// The below import statement initializes dns-caching
|
// The below import statement initializes dns-caching
|
||||||
import "./dns";
|
import "./dns";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "fetchAgent" });
|
||||||
|
|
||||||
export class LoggingAgent extends Agent {
|
export class LoggingAgent extends Agent {
|
||||||
constructor(...props: ConstructorParameters<typeof Agent>) {
|
constructor(...props: ConstructorParameters<typeof Agent>) {
|
||||||
super(...props);
|
super(...props);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Dispatcher } from "undici";
|
import type { Dispatcher } from "undici";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import * as logs from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { LoggingAgent } from "../fetch-agent";
|
import { LoggingAgent } from "../fetch-agent";
|
||||||
|
|
||||||
@@ -16,24 +16,36 @@ vi.mock("undici", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/logs", async () => {
|
||||||
|
const actual: typeof logs = await vi.importActual("@homarr/core/infrastructure/logs");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
createLogger: vi.fn().mockReturnValue({
|
||||||
|
debug: vi.fn(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const REDACTED = "REDACTED";
|
const REDACTED = "REDACTED";
|
||||||
|
|
||||||
|
const loggerMock = logs.createLogger({ module: "test" });
|
||||||
|
|
||||||
describe("LoggingAgent should log all requests", () => {
|
describe("LoggingAgent should log all requests", () => {
|
||||||
test("should log all requests", () => {
|
test("should log all requests", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
const debugSpy = vi.spyOn(loggerMock, "debug");
|
||||||
const agent = new LoggingAgent();
|
const agent = new LoggingAgent();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
agent.dispatch({ origin: "https://homarr.dev", path: "/", method: "GET" }, {});
|
agent.dispatch({ origin: "https://homarr.dev", path: "/", method: "GET" }, {});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(infoLogSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)");
|
expect(debugSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show amount of headers", () => {
|
test("should show amount of headers", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
const debugSpy = vi.spyOn(loggerMock, "debug");
|
||||||
const agent = new LoggingAgent();
|
const agent = new LoggingAgent();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -51,7 +63,7 @@ describe("LoggingAgent should log all requests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)"));
|
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
@@ -69,14 +81,14 @@ describe("LoggingAgent should log all requests", () => {
|
|||||||
[`/${"a".repeat(32)}/?param=123`, `/${REDACTED}/?param=123`],
|
[`/${"a".repeat(32)}/?param=123`, `/${REDACTED}/?param=123`],
|
||||||
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
|
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
const debugSpy = vi.spyOn(loggerMock, "debug");
|
||||||
const agent = new LoggingAgent();
|
const agent = new LoggingAgent();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
|
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `));
|
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `));
|
||||||
});
|
});
|
||||||
test.each([
|
test.each([
|
||||||
["empty", "/?empty"],
|
["empty", "/?empty"],
|
||||||
@@ -88,13 +100,13 @@ describe("LoggingAgent should log all requests", () => {
|
|||||||
["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
|
["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
|
||||||
])("should not redact values that are %s", (_reason, path) => {
|
])("should not redact values that are %s", (_reason, path) => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
const debugSpy = vi.spyOn(loggerMock, "debug");
|
||||||
const agent = new LoggingAgent();
|
const agent = new LoggingAgent();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
|
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `));
|
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
"./infrastructure/redis": "./src/infrastructure/redis/client.ts",
|
"./infrastructure/redis": "./src/infrastructure/redis/client.ts",
|
||||||
"./infrastructure/env": "./src/infrastructure/env/index.ts",
|
"./infrastructure/env": "./src/infrastructure/env/index.ts",
|
||||||
".": "./src/index.ts"
|
"./infrastructure/logs": "./src/infrastructure/logs/index.ts",
|
||||||
|
"./infrastructure/logs/constants": "./src/infrastructure/logs/constants.ts",
|
||||||
|
"./infrastructure/logs/env": "./src/infrastructure/logs/env.ts",
|
||||||
|
"./infrastructure/logs/error": "./src/infrastructure/logs/error.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -26,6 +29,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
|
"superjson": "2.2.6",
|
||||||
|
"winston": "3.19.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
11
packages/core/src/infrastructure/logs/env.ts
Normal file
11
packages/core/src/infrastructure/logs/env.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { createEnv, runtimeEnvWithPrefix } from "../env";
|
||||||
|
import { logLevels } from "./constants";
|
||||||
|
|
||||||
|
export const logsEnv = createEnv({
|
||||||
|
server: {
|
||||||
|
LEVEL: z.enum(logLevels).default("info"),
|
||||||
|
},
|
||||||
|
runtimeEnv: runtimeEnvWithPrefix("LOG_"),
|
||||||
|
});
|
||||||
9
packages/core/src/infrastructure/logs/error.ts
Normal file
9
packages/core/src/infrastructure/logs/error.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class ErrorWithMetadata extends Error {
|
||||||
|
public metadata: Record<string, unknown>;
|
||||||
|
|
||||||
|
constructor(message: string, metadata: Record<string, unknown> = {}, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
this.name = "Error";
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { logsEnv } from "../env";
|
||||||
import { formatMetadata } from "./metadata";
|
import { formatMetadata } from "./metadata";
|
||||||
|
|
||||||
|
const ERROR_OBJECT_PRUNE_DEPTH = logsEnv.LEVEL === "debug" ? 10 : 3;
|
||||||
|
const ERROR_STACK_LINE_LIMIT = logsEnv.LEVEL === "debug" ? undefined : 5;
|
||||||
|
const ERROR_CAUSE_DEPTH = logsEnv.LEVEL === "debug" ? 10 : 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the cause of an error in the format
|
* Formats the cause of an error in the format
|
||||||
* @example caused by Error: {message}
|
* @example caused by Error: {message}
|
||||||
@@ -10,7 +15,7 @@ import { formatMetadata } from "./metadata";
|
|||||||
*/
|
*/
|
||||||
export const formatErrorCause = (cause: unknown, iteration = 0): string => {
|
export const formatErrorCause = (cause: unknown, iteration = 0): string => {
|
||||||
// Prevent infinite recursion
|
// Prevent infinite recursion
|
||||||
if (iteration > 5) {
|
if (iteration > ERROR_CAUSE_DEPTH) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +27,12 @@ export const formatErrorCause = (cause: unknown, iteration = 0): string => {
|
|||||||
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
|
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cause instanceof Object) {
|
if (typeof cause === "object" && cause !== null) {
|
||||||
return `\ncaused by ${JSON.stringify(cause)}`;
|
if ("cause" in cause) {
|
||||||
|
const { cause: innerCause, ...rest } = cause;
|
||||||
|
return `\ncaused by ${JSON.stringify(prune(rest, ERROR_OBJECT_PRUNE_DEPTH))}${formatErrorCause(innerCause, iteration + 1)}`;
|
||||||
|
}
|
||||||
|
return `\ncaused by ${JSON.stringify(prune(cause, ERROR_OBJECT_PRUNE_DEPTH))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `\ncaused by ${cause as string}`;
|
return `\ncaused by ${cause as string}`;
|
||||||
@@ -50,5 +59,28 @@ export const formatErrorTitle = (error: Error) => {
|
|||||||
* @param stack stack trace
|
* @param stack stack trace
|
||||||
* @returns formatted stack trace
|
* @returns formatted stack trace
|
||||||
*/
|
*/
|
||||||
export const formatErrorStack = (stack: string | undefined) => (stack ? removeFirstLine(stack) : "");
|
export const formatErrorStack = (stack: string | undefined) =>
|
||||||
const removeFirstLine = (stack: string) => stack.split("\n").slice(1).join("\n");
|
stack
|
||||||
|
?.split("\n")
|
||||||
|
.slice(1, ERROR_STACK_LINE_LIMIT ? ERROR_STACK_LINE_LIMIT + 1 : undefined)
|
||||||
|
.join("\n") ?? "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes nested properties from an object beyond a certain depth
|
||||||
|
*/
|
||||||
|
const prune = (value: unknown, depth: number): unknown => {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (depth === 0) return [];
|
||||||
|
return value.map((item) => prune(item, depth - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, prune(val, depth - 1)]));
|
||||||
|
};
|
||||||
25
packages/core/src/infrastructure/logs/format/index.ts
Normal file
25
packages/core/src/infrastructure/logs/format/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { format } from "winston";
|
||||||
|
|
||||||
|
import { formatErrorCause, formatErrorStack } from "./error";
|
||||||
|
import { formatMetadata } from "./metadata";
|
||||||
|
|
||||||
|
export const logFormat = format.combine(
|
||||||
|
format.colorize(),
|
||||||
|
format.timestamp(),
|
||||||
|
format.errors({ stack: true, cause: true }),
|
||||||
|
format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => {
|
||||||
|
const firstLine = `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}`;
|
||||||
|
|
||||||
|
if (!cause && !stack) {
|
||||||
|
return firstLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatedStack = formatErrorStack(stack as string | undefined);
|
||||||
|
|
||||||
|
if (!cause) {
|
||||||
|
return `${firstLine}\n${formatedStack}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${firstLine}\n${formatedStack}${formatErrorCause(cause)}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
|
||||||
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
|
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
|
||||||
const filteredMetadata = Object.keys(metadata)
|
const metadataObject = metadata instanceof ErrorWithMetadata ? metadata.metadata : metadata;
|
||||||
|
|
||||||
|
const filteredMetadata = Object.keys(metadataObject)
|
||||||
.filter((key) => !ignoreKeys?.includes(key))
|
.filter((key) => !ignoreKeys?.includes(key))
|
||||||
.map((key) => ({ key, value: metadata[key as keyof typeof metadata] }))
|
.map((key) => ({ key, value: metadataObject[key as keyof typeof metadataObject] }))
|
||||||
.filter(({ value }) => typeof value !== "object" && typeof value !== "function");
|
.filter(({ value }) => typeof value !== "object" && typeof value !== "function");
|
||||||
|
|
||||||
return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" ");
|
return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" ");
|
||||||
18
packages/core/src/infrastructure/logs/index.ts
Normal file
18
packages/core/src/infrastructure/logs/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
import { logsEnv } from "./env";
|
||||||
|
import { logFormat } from "./format";
|
||||||
|
import { logTransports } from "./transports";
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
format: logFormat,
|
||||||
|
transports: logTransports,
|
||||||
|
level: logsEnv.LEVEL,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DefaultMetadata {
|
||||||
|
module: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLogger = (metadata: DefaultMetadata & Record<string, unknown>) => logger.child(metadata);
|
||||||
|
export type Logger = winston.Logger;
|
||||||
21
packages/core/src/infrastructure/logs/transports/index.ts
Normal file
21
packages/core/src/infrastructure/logs/transports/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { transports } from "winston";
|
||||||
|
import type { transport } from "winston";
|
||||||
|
|
||||||
|
import { RedisTransport } from "./redis-transport";
|
||||||
|
|
||||||
|
const getTransports = () => {
|
||||||
|
const defaultTransports: transport[] = [new transports.Console()];
|
||||||
|
|
||||||
|
// Only add the Redis transport if we are not in CI
|
||||||
|
if (!(Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS))) {
|
||||||
|
return defaultTransports.concat(
|
||||||
|
new RedisTransport({
|
||||||
|
level: "debug",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTransports;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logTransports = getTransports();
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import Transport from "winston-transport";
|
import Transport from "winston-transport";
|
||||||
|
|
||||||
import type { RedisClient } from "@homarr/core/infrastructure/redis";
|
import type { RedisClient } from "../../redis/client";
|
||||||
import { createRedisClient } from "@homarr/core/infrastructure/redis";
|
import { createRedisClient } from "../../redis/client";
|
||||||
|
|
||||||
const messageSymbol = Symbol.for("message");
|
const messageSymbol = Symbol.for("message");
|
||||||
const levelSymbol = Symbol.for("level");
|
const levelSymbol = Symbol.for("level");
|
||||||
@@ -13,6 +13,7 @@ const levelSymbol = Symbol.for("level");
|
|||||||
//
|
//
|
||||||
export class RedisTransport extends Transport {
|
export class RedisTransport extends Transport {
|
||||||
private redis: RedisClient | null = null;
|
private redis: RedisClient | null = null;
|
||||||
|
public static readonly publishChannel = "pubSub:logging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the info to the Redis channel
|
* Log the info to the Redis channel
|
||||||
@@ -27,7 +28,7 @@ export class RedisTransport extends Transport {
|
|||||||
|
|
||||||
this.redis
|
this.redis
|
||||||
.publish(
|
.publish(
|
||||||
"pubSub:logging",
|
RedisTransport.publishChannel,
|
||||||
superjson.stringify({
|
superjson.stringify({
|
||||||
message: info[messageSymbol],
|
message: info[messageSymbol],
|
||||||
level: info[levelSymbol],
|
level: info[levelSymbol],
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@trpc/client": "^11.7.2",
|
"@trpc/client": "^11.7.2",
|
||||||
"@trpc/server": "^11.7.2",
|
"@trpc/server": "^11.7.2",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"node-cron": "^4.2.1"
|
"node-cron": "^4.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AxiosError } from "axios";
|
|
||||||
import { createTask, validate } from "node-cron";
|
import { createTask, validate } from "node-cron";
|
||||||
|
|
||||||
import { Stopwatch } from "@homarr/common";
|
import { Stopwatch } from "@homarr/common";
|
||||||
import type { MaybePromise } from "@homarr/common/types";
|
import type { MaybePromise } from "@homarr/common/types";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
import type { Logger } from "./logger";
|
import type { Logger } from "./logger";
|
||||||
@@ -33,33 +33,39 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
|||||||
return (callback: () => MaybePromise<void>) => {
|
return (callback: () => MaybePromise<void>) => {
|
||||||
const catchingCallbackAsync = async () => {
|
const catchingCallbackAsync = async () => {
|
||||||
try {
|
try {
|
||||||
creatorOptions.logger.logDebug(`The callback of '${name}' cron job started`);
|
creatorOptions.logger.logDebug("The callback of cron job started", {
|
||||||
|
name,
|
||||||
|
});
|
||||||
const stopwatch = new Stopwatch();
|
const stopwatch = new Stopwatch();
|
||||||
await creatorOptions.beforeCallback?.(name);
|
await creatorOptions.beforeCallback?.(name);
|
||||||
const beforeCallbackTook = stopwatch.getElapsedInHumanWords();
|
const beforeCallbackTook = stopwatch.getElapsedInHumanWords();
|
||||||
await callback();
|
await callback();
|
||||||
const callbackTook = stopwatch.getElapsedInHumanWords();
|
const callbackTook = stopwatch.getElapsedInHumanWords();
|
||||||
creatorOptions.logger.logDebug(
|
creatorOptions.logger.logDebug("The callback of cron job succeeded", {
|
||||||
`The callback of '${name}' cron job succeeded (before callback took ${beforeCallbackTook}, callback took ${callbackTook})`,
|
name,
|
||||||
);
|
beforeCallbackTook,
|
||||||
|
callbackTook,
|
||||||
|
});
|
||||||
|
|
||||||
const durationInMillis = stopwatch.getElapsedInMilliseconds();
|
const durationInMillis = stopwatch.getElapsedInMilliseconds();
|
||||||
if (durationInMillis > expectedMaximumDurationInMillis) {
|
if (durationInMillis > expectedMaximumDurationInMillis) {
|
||||||
creatorOptions.logger.logWarning(
|
creatorOptions.logger.logWarning("The callback of cron job took longer than expected", {
|
||||||
`The callback of '${name}' succeeded but took ${(durationInMillis - expectedMaximumDurationInMillis).toFixed(2)}ms longer than expected (${expectedMaximumDurationInMillis}ms). This may indicate that your network performance, host performance or something else is too slow. If this happens too often, it should be looked into.`,
|
name,
|
||||||
);
|
durationInMillis,
|
||||||
|
expectedMaximumDurationInMillis,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await creatorOptions.onCallbackSuccess?.(name);
|
await creatorOptions.onCallbackSuccess?.(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log AxiosError in a less detailed way to prevent very long output
|
creatorOptions.logger.logError(
|
||||||
if (error instanceof AxiosError) {
|
new ErrorWithMetadata(
|
||||||
creatorOptions.logger.logError(
|
"The callback of cron job failed",
|
||||||
`Failed to run job '${name}': [AxiosError] ${error.message} ${error.response?.status} ${error.response?.config.url}\n${error.stack}`,
|
{
|
||||||
);
|
name,
|
||||||
} else {
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
{ cause: error },
|
||||||
creatorOptions.logger.logError(`Failed to run job '${name}': ${error}`);
|
),
|
||||||
}
|
);
|
||||||
await creatorOptions.onCallbackError?.(name, error);
|
await creatorOptions.onCallbackError?.(name, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -80,21 +86,28 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
|||||||
timezone: creatorOptions.timezone,
|
timezone: creatorOptions.timezone,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
creatorOptions.logger.logDebug(
|
creatorOptions.logger.logDebug("The scheduled task for cron job was created", {
|
||||||
`The cron job '${name}' was created with expression ${defaultCronExpression} in timezone ${creatorOptions.timezone} and runOnStart ${options.runOnStart}`,
|
name,
|
||||||
);
|
cronExpression: defaultCronExpression,
|
||||||
|
timezone: creatorOptions.timezone,
|
||||||
|
runOnStart: options.runOnStart,
|
||||||
|
});
|
||||||
|
|
||||||
return scheduledTask;
|
return scheduledTask;
|
||||||
},
|
},
|
||||||
async onStartAsync() {
|
async onStartAsync() {
|
||||||
if (options.beforeStart) {
|
if (options.beforeStart) {
|
||||||
creatorOptions.logger.logDebug(`Running beforeStart for job: ${name}`);
|
creatorOptions.logger.logDebug("Running beforeStart for job", {
|
||||||
|
name,
|
||||||
|
});
|
||||||
await options.beforeStart();
|
await options.beforeStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.runOnStart) return;
|
if (!options.runOnStart) return;
|
||||||
|
|
||||||
creatorOptions.logger.logDebug(`The cron job '${name}' is running because runOnStart is set to true`);
|
creatorOptions.logger.logDebug("The cron job is configured to run on start, executing callback", {
|
||||||
|
name,
|
||||||
|
});
|
||||||
await catchingCallbackAsync();
|
await catchingCallbackAsync();
|
||||||
},
|
},
|
||||||
async executeAsync() {
|
async executeAsync() {
|
||||||
@@ -117,11 +130,17 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
|||||||
defaultCronExpression: TExpression,
|
defaultCronExpression: TExpression,
|
||||||
options: CreateCronJobOptions = { runOnStart: false },
|
options: CreateCronJobOptions = { runOnStart: false },
|
||||||
) => {
|
) => {
|
||||||
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
|
creatorOptions.logger.logDebug("Validating cron expression for cron job", {
|
||||||
|
name,
|
||||||
|
cronExpression: defaultCronExpression,
|
||||||
|
});
|
||||||
if (!validate(defaultCronExpression)) {
|
if (!validate(defaultCronExpression)) {
|
||||||
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
||||||
}
|
}
|
||||||
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
|
creatorOptions.logger.logDebug("Cron job expression for cron job is valid", {
|
||||||
|
name,
|
||||||
|
cronExpression: defaultCronExpression,
|
||||||
|
});
|
||||||
|
|
||||||
const returnValue = {
|
const returnValue = {
|
||||||
withCallback: createCallback<TAllowedNames, TName>(name, defaultCronExpression, options, creatorOptions),
|
withCallback: createCallback<TAllowedNames, TName>(name, defaultCronExpression, options, creatorOptions),
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
options: CreateCronJobGroupCreatorOptions,
|
options: CreateCronJobGroupCreatorOptions,
|
||||||
) => {
|
) => {
|
||||||
return <TJobs extends Jobs<TAllowedNames>>(jobs: TJobs) => {
|
return <TJobs extends Jobs<TAllowedNames>>(jobs: TJobs) => {
|
||||||
options.logger.logDebug(`Creating job group with ${Object.keys(jobs).length} jobs.`);
|
options.logger.logDebug("Creating job group.", {
|
||||||
|
jobCount: Object.keys(jobs).length,
|
||||||
|
});
|
||||||
for (const [key, job] of objectEntries(jobs)) {
|
for (const [key, job] of objectEntries(jobs)) {
|
||||||
if (typeof key !== "string" || typeof job.name !== "string") continue;
|
if (typeof key !== "string" || typeof job.name !== "string") continue;
|
||||||
|
|
||||||
options.logger.logDebug(`Added job ${job.name} to the job registry.`);
|
options.logger.logDebug("Registering job in the job registry.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
jobRegistry.set(key, {
|
jobRegistry.set(key, {
|
||||||
...job,
|
...job,
|
||||||
name: job.name,
|
name: job.name,
|
||||||
@@ -54,7 +58,9 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
if (!job) return;
|
if (!job) return;
|
||||||
if (!tasks.has(job.name)) return;
|
if (!tasks.has(job.name)) return;
|
||||||
|
|
||||||
options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
|
options.logger.logInfo("Starting schedule of cron job.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
await job.onStartAsync();
|
await job.onStartAsync();
|
||||||
await tasks.get(name as string)?.start();
|
await tasks.get(name as string)?.start();
|
||||||
},
|
},
|
||||||
@@ -64,7 +70,9 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
|
options.logger.logInfo("Starting schedule of cron job.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
await job.onStartAsync();
|
await job.onStartAsync();
|
||||||
await tasks.get(job.name)?.start();
|
await tasks.get(job.name)?.start();
|
||||||
}
|
}
|
||||||
@@ -76,19 +84,25 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
throw new Error(`The job "${job.name}" can not be executed manually.`);
|
throw new Error(`The job "${job.name}" can not be executed manually.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
|
options.logger.logInfo("Running schedule cron job manually.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
await tasks.get(name as string)?.execute();
|
await tasks.get(name as string)?.execute();
|
||||||
},
|
},
|
||||||
stopAsync: async (name: keyof TJobs) => {
|
stopAsync: async (name: keyof TJobs) => {
|
||||||
const job = jobRegistry.get(name as string);
|
const job = jobRegistry.get(name as string);
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
|
|
||||||
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
|
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
await tasks.get(name as string)?.stop();
|
await tasks.get(name as string)?.stop();
|
||||||
},
|
},
|
||||||
stopAllAsync: async () => {
|
stopAllAsync: async () => {
|
||||||
for (const job of jobRegistry.values()) {
|
for (const job of jobRegistry.values()) {
|
||||||
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
|
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||||
|
name: job.name,
|
||||||
|
});
|
||||||
await tasks.get(job.name)?.stop();
|
await tasks.get(job.name)?.stop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { CreateCronJobCreatorOptions } from "./creator";
|
import type { CreateCronJobCreatorOptions } from "./creator";
|
||||||
import { createCronJobCreator } from "./creator";
|
import { createCronJobCreator } from "./creator";
|
||||||
import { createJobGroupCreator } from "./group";
|
import { createJobGroupCreator } from "./group";
|
||||||
import { ConsoleLogger } from "./logger";
|
|
||||||
|
|
||||||
export const createCronJobFunctions = <TAllowedNames extends string>(
|
export const createCronJobFunctions = <TAllowedNames extends string>(
|
||||||
options: CreateCronJobCreatorOptions<TAllowedNames> = { logger: new ConsoleLogger() },
|
options: CreateCronJobCreatorOptions<TAllowedNames>,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
createCronJob: createCronJobCreator<TAllowedNames>(options),
|
createCronJob: createCronJobCreator<TAllowedNames>(options),
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
export interface Logger {
|
export interface Logger {
|
||||||
logDebug(message: string): void;
|
logDebug(message: string, metadata?: Record<string, unknown>): void;
|
||||||
logInfo(message: string): void;
|
logInfo(message: string, metadata?: Record<string, unknown>): void;
|
||||||
|
logError(message: string, metadata?: Record<string, unknown>): void;
|
||||||
logError(error: unknown): void;
|
logError(error: unknown): void;
|
||||||
logWarning(message: string): void;
|
logWarning(message: string, metadata?: Record<string, unknown>): void;
|
||||||
}
|
|
||||||
|
|
||||||
export class ConsoleLogger implements Logger {
|
|
||||||
public logDebug(message: string) {
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public logInfo(message: string) {
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public logError(error: unknown) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
public logWarning(message: string) {
|
|
||||||
console.warn(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
"@homarr/analytics": "workspace:^0.1.0",
|
"@homarr/analytics": "workspace:^0.1.0",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@homarr/ping": "workspace:^0.1.0",
|
"@homarr/ping": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/request-handler": "workspace:^0.1.0",
|
"@homarr/request-handler": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
|
import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../../widgets";
|
import type { WidgetComponentProps } from "../../../widgets";
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "dockerJobs" });
|
||||||
|
|
||||||
export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => {
|
export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => {
|
||||||
const dockerItems = await db.query.items.findMany({
|
const dockerItems = await db.query.items.findMany({
|
||||||
where: eq(items.kind, "dockerContainers"),
|
where: eq(items.kind, "dockerContainers"),
|
||||||
@@ -21,7 +24,7 @@ export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUT
|
|||||||
const innerHandler = dockerContainersRequestHandler.handler(options);
|
const innerHandler = dockerContainersRequestHandler.handler(options);
|
||||||
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update Docker container status", { item, error });
|
logger.error(new ErrorWithMetadata("Failed to update Docker container status", { item }, { cause: error }));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { createId, splitToNChunks, Stopwatch } from "@homarr/common";
|
import { createId, splitToNChunks, Stopwatch } from "@homarr/common";
|
||||||
import { env } from "@homarr/common/env";
|
import { env } from "@homarr/common/env";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
||||||
import type { InferInsertModel } from "@homarr/db";
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db";
|
import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db";
|
||||||
import { iconRepositories, icons } from "@homarr/db/schema";
|
import { iconRepositories, icons } from "@homarr/db/schema";
|
||||||
import { fetchIconsAsync } from "@homarr/icons";
|
import { fetchIconsAsync } from "@homarr/icons";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "iconsUpdaterJobs" });
|
||||||
|
|
||||||
export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||||
runOnStart: true,
|
runOnStart: true,
|
||||||
expectedMaximumDurationInMillis: 10 * 1000,
|
expectedMaximumDurationInMillis: 10 * 1000,
|
||||||
@@ -21,9 +23,11 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
const countIcons = repositoryIconGroups
|
const countIcons = repositoryIconGroups
|
||||||
.map((group) => group.icons.length)
|
.map((group) => group.icons.length)
|
||||||
.reduce((partialSum, arrayLength) => partialSum + arrayLength, 0);
|
.reduce((partialSum, arrayLength) => partialSum + arrayLength, 0);
|
||||||
logger.info(
|
logger.info("Fetched icons from repositories", {
|
||||||
`Successfully fetched ${countIcons} icons from ${repositoryIconGroups.length} repositories within ${stopWatch.getElapsedInHumanWords()}`,
|
repositoryCount: repositoryIconGroups.length,
|
||||||
);
|
iconCount: countIcons,
|
||||||
|
duration: stopWatch.getElapsedInHumanWords(),
|
||||||
|
});
|
||||||
|
|
||||||
const databaseIconRepositories = await db.query.iconRepositories.findMany({
|
const databaseIconRepositories = await db.query.iconRepositories.findMany({
|
||||||
with: {
|
with: {
|
||||||
@@ -162,5 +166,9 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Updated database within ${stopWatch.getElapsedInHumanWords()} (-${countDeleted}, +${countInserted})`);
|
logger.info("Updated icons in database", {
|
||||||
|
duration: stopWatch.getElapsedInHumanWords(),
|
||||||
|
added: countInserted,
|
||||||
|
deleted: countDeleted,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { sendPingRequestAsync } from "@homarr/ping";
|
import { sendPingRequestAsync } from "@homarr/ping";
|
||||||
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
||||||
|
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "pingJobs" });
|
||||||
|
|
||||||
const resetPreviousUrlsAsync = async () => {
|
const resetPreviousUrlsAsync = async () => {
|
||||||
await pingUrlChannel.clearAsync();
|
await pingUrlChannel.clearAsync();
|
||||||
logger.info("Cleared previous ping urls");
|
logger.info("Cleared previous ping urls");
|
||||||
@@ -31,9 +34,9 @@ const pingAsync = async (url: string) => {
|
|||||||
const pingResult = await sendPingRequestAsync(url);
|
const pingResult = await sendPingRequestAsync(url);
|
||||||
|
|
||||||
if ("statusCode" in pingResult) {
|
if ("statusCode" in pingResult) {
|
||||||
logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`);
|
logger.debug("Executed ping successfully", { url, statusCode: pingResult.statusCode });
|
||||||
} else {
|
} else {
|
||||||
logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`);
|
logger.error(new ErrorWithMetadata("Executing ping failed", { url }, { cause: pingResult.error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
await pingChannel.publishAsync({
|
await pingChannel.publishAsync({
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
// This import is done that way to avoid circular dependencies.
|
// This import is done that way to avoid circular dependencies.
|
||||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../../widgets";
|
import type { WidgetComponentProps } from "../../../widgets";
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "rssFeedsJobs" });
|
||||||
|
|
||||||
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => {
|
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => {
|
||||||
const rssItems = await db.query.items.findMany({
|
const rssItems = await db.query.items.findMany({
|
||||||
where: eq(items.kind, "rssFeed"),
|
where: eq(items.kind, "rssFeed"),
|
||||||
@@ -29,7 +32,7 @@ export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallb
|
|||||||
forceUpdate: true,
|
forceUpdate: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update RSS feed", { url, error });
|
logger.error(new ErrorWithMetadata("Failed to update RSS feed", { url }, { cause: error }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../../widgets";
|
import type { WidgetComponentProps } from "../../../widgets";
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "weatherJobs" });
|
||||||
|
|
||||||
export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => {
|
export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => {
|
||||||
const weatherItems = await db.query.items.findMany({
|
const weatherItems = await db.query.items.findMany({
|
||||||
where: eq(items.kind, "weather"),
|
where: eq(items.kind, "weather"),
|
||||||
@@ -27,7 +30,7 @@ export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallbac
|
|||||||
});
|
});
|
||||||
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update weather", { id: item.id, error });
|
logger.error(new ErrorWithMetadata("Failed to update weather", { id: item.id }, { cause: error }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { beforeCallbackAsync, onCallbackErrorAsync, onCallbackSuccessAsync } from "@homarr/cron-job-status/publisher";
|
import { beforeCallbackAsync, onCallbackErrorAsync, onCallbackSuccessAsync } from "@homarr/cron-job-status/publisher";
|
||||||
import { createCronJobFunctions } from "@homarr/cron-jobs-core";
|
import { createCronJobFunctions } from "@homarr/cron-jobs-core";
|
||||||
import type { Logger } from "@homarr/cron-jobs-core/logger";
|
import type { Logger } from "@homarr/cron-jobs-core/logger";
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "cronJobs" });
|
||||||
|
|
||||||
class WinstonCronJobLogger implements Logger {
|
class WinstonCronJobLogger implements Logger {
|
||||||
logDebug(message: string) {
|
logDebug(message: string, metadata?: Record<string, unknown>): void {
|
||||||
logger.debug(message);
|
logger.debug(message, metadata);
|
||||||
}
|
}
|
||||||
|
logInfo(message: string, metadata?: Record<string, unknown>): void {
|
||||||
logInfo(message: string) {
|
logger.info(message, metadata);
|
||||||
logger.info(message);
|
|
||||||
}
|
}
|
||||||
|
logError(message: string, metadata?: Record<string, unknown>): void;
|
||||||
logError(error: unknown) {
|
logError(error: unknown): void;
|
||||||
logger.error(error);
|
logError(messageOrError: unknown, metadata?: Record<string, unknown>): void {
|
||||||
|
if (typeof messageOrError === "string") {
|
||||||
|
logger.error(messageOrError, metadata);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.error(messageOrError);
|
||||||
}
|
}
|
||||||
|
logWarning(message: string, metadata?: Record<string, unknown>): void {
|
||||||
logWarning(message: string) {
|
logger.warn(message, metadata);
|
||||||
logger.warn(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import type { Pool as MysqlConnectionPool } from "mysql2";
|
|||||||
import mysql from "mysql2";
|
import mysql from "mysql2";
|
||||||
import { Pool as PostgresPool } from "pg";
|
import { Pool as PostgresPool } from "pg";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
import * as mysqlSchema from "./schema/mysql";
|
import * as mysqlSchema from "./schema/mysql";
|
||||||
import * as pgSchema from "./schema/postgresql";
|
import * as pgSchema from "./schema/postgresql";
|
||||||
import * as sqliteSchema from "./schema/sqlite";
|
import * as sqliteSchema from "./schema/sqlite";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "db" });
|
||||||
|
|
||||||
export type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
export type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
||||||
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
|
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
|
||||||
export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>;
|
export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>;
|
||||||
@@ -44,7 +46,7 @@ export let database: HomarrDatabase;
|
|||||||
|
|
||||||
class WinstonDrizzleLogger implements Logger {
|
class WinstonDrizzleLogger implements Logger {
|
||||||
logQuery(query: string, _: unknown[]): void {
|
logQuery(query: string, _: unknown[]): void {
|
||||||
logger.debug(`Executed SQL query: ${query}`);
|
logger.debug("Executed SQL query", { query });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.10",
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0"
|
"@homarr/db": "workspace:^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
|
||||||
import type { IconRepositoryLicense } from "../types/icon-repository-license";
|
import type { IconRepositoryLicense } from "../types/icon-repository-license";
|
||||||
import type { RepositoryIconGroup } from "../types/repository-icon-group";
|
import type { RepositoryIconGroup } from "../types/repository-icon-group";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "iconRepository" });
|
||||||
|
|
||||||
export abstract class IconRepository {
|
export abstract class IconRepository {
|
||||||
protected readonly allowedImageFileTypes = [".png", ".svg", ".jpeg"];
|
protected readonly allowedImageFileTypes = [".png", ".svg", ".jpeg"];
|
||||||
|
|
||||||
@@ -19,7 +22,9 @@ export abstract class IconRepository {
|
|||||||
try {
|
try {
|
||||||
return await this.getAllIconsInternalAsync();
|
return await this.getAllIconsInternalAsync();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Unable to request icons from repository "${this.slug}": ${JSON.stringify(err)}`);
|
logger.error(
|
||||||
|
new ErrorWithMetadata("Unable to request icons from repository", { slug: this.slug }, { cause: err }),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
icons: [],
|
icons: [],
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/certificates": "workspace:^0.1.0",
|
"@homarr/certificates": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"bcrypt": "^6.0.0"
|
"bcrypt": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import bcrypt from "bcrypt";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { createGetSetChannel } from "@homarr/redis";
|
import { createGetSetChannel } from "@homarr/redis";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "imageProxy" });
|
||||||
|
|
||||||
const createHashChannel = (hash: `${string}.${string}`) => createGetSetChannel<string>(`image-proxy:hash:${hash}`);
|
const createHashChannel = (hash: `${string}.${string}`) => createGetSetChannel<string>(`image-proxy:hash:${hash}`);
|
||||||
const createUrlByIdChannel = (id: string) =>
|
const createUrlByIdChannel = (id: string) =>
|
||||||
createGetSetChannel<{
|
createGetSetChannel<{
|
||||||
@@ -25,7 +28,7 @@ export class ImageProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
const salt = await bcrypt.genSalt(10);
|
||||||
logger.debug(`Generated new salt for image proxy salt="${salt}"`);
|
logger.debug("Generated new salt for image proxy", { salt });
|
||||||
ImageProxy.salt = salt;
|
ImageProxy.salt = salt;
|
||||||
await saltChannel.setAsync(salt);
|
await saltChannel.setAsync(salt);
|
||||||
return salt;
|
return salt;
|
||||||
@@ -34,9 +37,11 @@ export class ImageProxy {
|
|||||||
public async createImageAsync(url: string, headers?: Record<string, string>): Promise<string> {
|
public async createImageAsync(url: string, headers?: Record<string, string>): Promise<string> {
|
||||||
const existingId = await this.getExistingIdAsync(url, headers);
|
const existingId = await this.getExistingIdAsync(url, headers);
|
||||||
if (existingId) {
|
if (existingId) {
|
||||||
logger.debug(
|
logger.debug("Image already exists in the proxy", {
|
||||||
`Image already exists in the proxy id="${existingId}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`,
|
id: existingId,
|
||||||
);
|
url: this.redactUrl(url),
|
||||||
|
headers: this.redactHeaders(headers ?? null),
|
||||||
|
});
|
||||||
return this.createImageUrl(existingId);
|
return this.createImageUrl(existingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +64,25 @@ export class ImageProxy {
|
|||||||
const proxyUrl = this.createImageUrl(id);
|
const proxyUrl = this.createImageUrl(id);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to fetch image id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl}" statusCode="${response.status}"`,
|
new ErrorWithMetadata("Failed to fetch image", {
|
||||||
|
id,
|
||||||
|
url: this.redactUrl(urlAndHeaders.url),
|
||||||
|
headers: this.redactHeaders(urlAndHeaders.headers),
|
||||||
|
proxyUrl,
|
||||||
|
statusCode: response.status,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = (await response.blob()) as Blob;
|
const blob = (await response.blob()) as Blob;
|
||||||
logger.debug(
|
logger.debug("Forwarding image succeeded", {
|
||||||
`Forwarding image succeeded id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl} size="${(blob.size / 1024).toFixed(1)}KB"`,
|
id,
|
||||||
);
|
url: this.redactUrl(urlAndHeaders.url),
|
||||||
|
headers: this.redactHeaders(urlAndHeaders.headers),
|
||||||
|
proxyUrl,
|
||||||
|
size: `${(blob.size / 1024).toFixed(1)}KB`,
|
||||||
|
});
|
||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
@@ -80,7 +95,7 @@ export class ImageProxy {
|
|||||||
const urlHeaderChannel = createUrlByIdChannel(id);
|
const urlHeaderChannel = createUrlByIdChannel(id);
|
||||||
const urlHeader = await urlHeaderChannel.getAsync();
|
const urlHeader = await urlHeaderChannel.getAsync();
|
||||||
if (!urlHeader) {
|
if (!urlHeader) {
|
||||||
logger.warn(`Image not found in the proxy id="${id}"`);
|
logger.warn("Image not found in the proxy", { id });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +127,11 @@ export class ImageProxy {
|
|||||||
});
|
});
|
||||||
await hashChannel.setAsync(id);
|
await hashChannel.setAsync(id);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug("Stored image in the proxy", {
|
||||||
`Stored image in the proxy id="${id}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`,
|
id,
|
||||||
);
|
url: this.redactUrl(url),
|
||||||
|
headers: this.redactHeaders(headers ?? null),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private redactUrl(url: string): string {
|
private redactUrl(url: string): string {
|
||||||
|
|||||||
@@ -31,10 +31,10 @@
|
|||||||
"@gitbeaker/rest": "^43.8.0",
|
"@gitbeaker/rest": "^43.8.0",
|
||||||
"@homarr/certificates": "workspace:^0.1.0",
|
"@homarr/certificates": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/image-proxy": "workspace:^0.1.0",
|
"@homarr/image-proxy": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
|
||||||
"@homarr/node-unifi": "^2.6.0",
|
"@homarr/node-unifi": "^2.6.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isFunction } from "@homarr/common";
|
import { isFunction } from "@homarr/common";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { Integration } from "../integration";
|
import type { Integration } from "../integration";
|
||||||
import type { IIntegrationErrorHandler } from "./handler";
|
import type { IIntegrationErrorHandler } from "./handler";
|
||||||
@@ -8,9 +8,7 @@ import { IntegrationError } from "./integration-error";
|
|||||||
import { IntegrationUnknownError } from "./integration-unknown-error";
|
import { IntegrationUnknownError } from "./integration-unknown-error";
|
||||||
import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./parse";
|
import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./parse";
|
||||||
|
|
||||||
const localLogger = logger.child({
|
const logger = createLogger({ module: "handleIntegrationErrors" });
|
||||||
module: "HandleIntegrationErrors",
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
|
||||||
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
|
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
|
||||||
@@ -59,7 +57,7 @@ export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the error was handled and should be thrown again, throw it
|
// If the error was handled and should be thrown again, throw it
|
||||||
localLogger.debug("Unhandled error in integration", {
|
logger.debug("Unhandled error in integration", {
|
||||||
error: error instanceof Error ? `${error.name}: ${error.message}` : undefined,
|
error: error instanceof Error ? `${error.name}: ${error.message}` : undefined,
|
||||||
integrationName: this.publicIntegration.name,
|
integrationName: this.publicIntegration.name,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { createGetSetChannel } from "@homarr/redis";
|
import { createGetSetChannel } from "@homarr/redis";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "SessionStore" });
|
const logger = createLogger({ module: "sessionStore" });
|
||||||
|
|
||||||
export const createSessionStore = <TValue>(integration: { id: string }) => {
|
export const createSessionStore = <TValue>(integration: { id: string }) => {
|
||||||
const channelName = `session-store:${integration.id}`;
|
const channelName = `session-store:${integration.id}`;
|
||||||
@@ -12,26 +13,26 @@ export const createSessionStore = <TValue>(integration: { id: string }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
async getAsync() {
|
async getAsync() {
|
||||||
localLogger.debug("Getting session from store", { store: channelName });
|
logger.debug("Getting session from store", { store: channelName });
|
||||||
const value = await channel.getAsync();
|
const value = await channel.getAsync();
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
try {
|
try {
|
||||||
return superjson.parse<TValue>(decryptSecret(value));
|
return superjson.parse<TValue>(decryptSecret(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.warn("Failed to load session", { store: channelName, error });
|
logger.warn("Failed to load session", { store: channelName, error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setAsync(value: TValue) {
|
async setAsync(value: TValue) {
|
||||||
localLogger.debug("Updating session in store", { store: channelName });
|
logger.debug("Updating session in store", { store: channelName });
|
||||||
try {
|
try {
|
||||||
await channel.setAsync(encryptSecret(superjson.stringify(value)));
|
await channel.setAsync(encryptSecret(superjson.stringify(value)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.error("Failed to save session", { store: channelName, error });
|
logger.error(new ErrorWithMetadata("Failed to save session", { store: channelName }, { cause: error }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async clearAsync() {
|
async clearAsync() {
|
||||||
localLogger.debug("Cleared session in store", { store: channelName });
|
logger.debug("Cleared session in store", { store: channelName });
|
||||||
await channel.removeAsync();
|
await channel.removeAsync();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getTrustedCertificateHostnamesAsync,
|
getTrustedCertificateHostnamesAsync,
|
||||||
} from "@homarr/certificates/server";
|
} from "@homarr/certificates/server";
|
||||||
import { getPortFromUrl } from "@homarr/common";
|
import { getPortFromUrl } from "@homarr/common";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { IntegrationRequestErrorOfType } from "../errors/http/integration-request-error";
|
import type { IntegrationRequestErrorOfType } from "../errors/http/integration-request-error";
|
||||||
import { IntegrationRequestError } from "../errors/http/integration-request-error";
|
import { IntegrationRequestError } from "../errors/http/integration-request-error";
|
||||||
@@ -15,8 +15,8 @@ import { IntegrationError } from "../errors/integration-error";
|
|||||||
import type { AnyTestConnectionError } from "./test-connection-error";
|
import type { AnyTestConnectionError } from "./test-connection-error";
|
||||||
import { TestConnectionError } from "./test-connection-error";
|
import { TestConnectionError } from "./test-connection-error";
|
||||||
|
|
||||||
const localLogger = logger.child({
|
const logger = createLogger({
|
||||||
module: "TestConnectionService",
|
module: "testConnectionService",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TestingResult =
|
export type TestingResult =
|
||||||
@@ -36,7 +36,7 @@ export class TestConnectionService {
|
|||||||
constructor(private url: URL) {}
|
constructor(private url: URL) {}
|
||||||
|
|
||||||
public async handleAsync(testingCallbackAsync: AsyncTestingCallback) {
|
public async handleAsync(testingCallbackAsync: AsyncTestingCallback) {
|
||||||
localLogger.debug("Testing connection", {
|
logger.debug("Testing connection", {
|
||||||
url: this.url.toString(),
|
url: this.url.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,14 +72,14 @@ export class TestConnectionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (testingResult.success) {
|
if (testingResult.success) {
|
||||||
localLogger.debug("Testing connection succeeded", {
|
logger.debug("Testing connection succeeded", {
|
||||||
url: this.url.toString(),
|
url: this.url.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return testingResult;
|
return testingResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
localLogger.debug("Testing connection failed", {
|
logger.debug("Testing connection failed", {
|
||||||
url: this.url.toString(),
|
url: this.url.toString(),
|
||||||
error: `${testingResult.error.name}: ${testingResult.error.message}`,
|
error: `${testingResult.error.name}: ${testingResult.error.message}`,
|
||||||
});
|
});
|
||||||
@@ -124,7 +124,7 @@ export class TestConnectionService {
|
|||||||
const x509 = socket.getPeerX509Certificate();
|
const x509 = socket.getPeerX509Certificate();
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
|
||||||
localLogger.debug("Fetched certificate", {
|
logger.debug("Fetched certificate", {
|
||||||
url: this.url.toString(),
|
url: this.url.toString(),
|
||||||
subject: x509?.subject,
|
subject: x509?.subject,
|
||||||
issuer: x509?.issuer,
|
issuer: x509?.issuer,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RequestInit, Response } from "undici";
|
import type { RequestInit, Response } from "undici";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "CodebergIntegration" });
|
const logger = createLogger({ module: "codebergIntegration" });
|
||||||
|
|
||||||
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||||
@@ -45,10 +45,9 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
private parseIdentifier(identifier: string) {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", {
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`,
|
identifier,
|
||||||
{ identifier },
|
});
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { owner, name };
|
return { owner, name };
|
||||||
@@ -109,7 +108,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
localLogger.warn(`Failed to get details response for ${owner}/${name} with Codeberg integration`, {
|
logger.warn("Failed to get details", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: response.statusText,
|
error: response.statusText,
|
||||||
@@ -122,7 +121,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
localLogger.warn(`Failed to parse details response for ${owner}/${name} with Codeberg integration`, {
|
logger.warn("Failed to parse details", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { fetch, RequestInit, Response } from "undici";
|
|||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
|
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "DockerHubIntegration" });
|
const logger = createLogger({ module: "dockerHubIntegration" });
|
||||||
|
|
||||||
export class DockerHubIntegration extends Integration implements ReleasesProviderIntegration {
|
export class DockerHubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
private readonly sessionStore: SessionStore<string>;
|
private readonly sessionStore: SessionStore<string>;
|
||||||
@@ -35,7 +35,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
const storedSession = await this.sessionStore.getAsync();
|
const storedSession = await this.sessionStore.getAsync();
|
||||||
|
|
||||||
if (storedSession) {
|
if (storedSession) {
|
||||||
localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
|
logger.debug("Using stored session for request", { integrationId: this.integration.id });
|
||||||
const response = await callback({
|
const response = await callback({
|
||||||
Authorization: `Bearer ${storedSession}`,
|
Authorization: `Bearer ${storedSession}`,
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id });
|
logger.debug("Session expired, getting new session", { integrationId: this.integration.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = await this.getSessionAsync();
|
const accessToken = await this.getSessionAsync();
|
||||||
@@ -57,10 +57,10 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
const hasAuth = this.hasSecretValue("username") && this.hasSecretValue("personalAccessToken");
|
const hasAuth = this.hasSecretValue("username") && this.hasSecretValue("personalAccessToken");
|
||||||
|
|
||||||
if (hasAuth) {
|
if (hasAuth) {
|
||||||
localLogger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id });
|
logger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id });
|
||||||
await this.getSessionAsync(input.fetchAsync);
|
await this.getSessionAsync(input.fetchAsync);
|
||||||
} else {
|
} else {
|
||||||
localLogger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id });
|
logger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id });
|
||||||
const response = await input.fetchAsync(this.url("/v2/repositories/library"));
|
const response = await input.fetchAsync(this.url("/v2/repositories/library"));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return TestConnectionError.StatusResult(response);
|
return TestConnectionError.StatusResult(response);
|
||||||
@@ -76,7 +76,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
if (!identifier.includes("/")) return { owner: "", name: identifier };
|
if (!identifier.includes("/")) return { owner: "", name: identifier };
|
||||||
const [owner, name] = identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(`Invalid identifier format. Expected 'owner/name' or 'name', for ${identifier} on DockerHub`, {
|
logger.warn("Invalid identifier format. Expected 'owner/name' or 'name', for identifier", {
|
||||||
identifier,
|
identifier,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -137,7 +137,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
localLogger.warn(`Failed to get details response for ${relativeUrl} with DockerHub integration`, {
|
logger.warn("Failed to get details response", {
|
||||||
relativeUrl,
|
relativeUrl,
|
||||||
error: response.statusText,
|
error: response.statusText,
|
||||||
});
|
});
|
||||||
@@ -149,7 +149,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
localLogger.warn(`Failed to parse details response for ${relativeUrl} with DockerHub integration`, {
|
logger.warn("Failed to parse details response", {
|
||||||
relativeUrl,
|
relativeUrl,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
@@ -183,7 +183,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
throw new ResponseError({ status: 401, url: response.url });
|
throw new ResponseError({ status: 401, url: response.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
localLogger.info("Received session successfully", { integrationId: this.integration.id });
|
logger.info("Received session successfully", { integrationId: this.integration.id });
|
||||||
|
|
||||||
return result.access_token;
|
return result.access_token;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Octokit, RequestError } from "octokit";
|
|||||||
import type { fetch } from "undici";
|
import type { fetch } from "undici";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
ReleaseResponse,
|
ReleaseResponse,
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
|
const logger = createLogger({ module: "githubContainerRegistryIntegration" });
|
||||||
|
|
||||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||||
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
|
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
@@ -45,10 +45,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
|||||||
private parseIdentifier(identifier: string) {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", { identifier });
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${identifier} with GitHub Container Registry integration`,
|
|
||||||
{ identifier },
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { owner, name };
|
return { owner, name };
|
||||||
@@ -91,7 +88,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
|||||||
return { success: true, data: { ...details, ...latestRelease } };
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, {
|
logger.warn("Failed to get releases", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -123,7 +120,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
|||||||
forksCount: response.data.repository?.forks_count,
|
forksCount: response.data.repository?.forks_count,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.warn(`Failed to get details for ${owner}\\${name} with GitHub Container Registry integration`, {
|
logger.warn("Failed to get details", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: error instanceof RequestError ? error.message : String(error),
|
error: error instanceof RequestError ? error.message : String(error),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Octokit, RequestError as OctokitRequestError } from "octokit";
|
|||||||
import type { fetch } from "undici";
|
import type { fetch } from "undici";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
ReleaseResponse,
|
ReleaseResponse,
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GithubIntegration" });
|
const logger = createLogger({ module: "githubIntegration" });
|
||||||
|
|
||||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||||
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
|
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
@@ -45,7 +45,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
private parseIdentifier(identifier: string) {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Github integration`, {
|
logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", {
|
||||||
identifier,
|
identifier,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -64,7 +64,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
const releasesResponse = await api.rest.repos.listReleases({ owner, repo: name });
|
const releasesResponse = await api.rest.repos.listReleases({ owner, repo: name });
|
||||||
|
|
||||||
if (releasesResponse.data.length === 0) {
|
if (releasesResponse.data.length === 0) {
|
||||||
localLogger.warn(`No releases found, for ${owner}/${name} with Github integration`, {
|
logger.warn("No releases found", {
|
||||||
identifier: `${owner}/${name}`,
|
identifier: `${owner}/${name}`,
|
||||||
});
|
});
|
||||||
return { success: false, error: { code: "noMatchingVersion" } };
|
return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
@@ -91,7 +91,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
return { success: true, data: { ...details, ...latestRelease } };
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
||||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
logger.warn("Failed to get releases", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -122,7 +122,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
forksCount: response.data.forks_count,
|
forksCount: response.data.forks_count,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, {
|
logger.warn("Failed to get details", {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: error instanceof OctokitRequestError ? error.message : String(error),
|
error: error instanceof OctokitRequestError ? error.message : String(error),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { FormattedResponse, RequestOptions, ResourceOptions } from "@gitbea
|
|||||||
import { Gitlab } from "@gitbeaker/rest";
|
import { Gitlab } from "@gitbeaker/rest";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
ReleaseResponse,
|
ReleaseResponse,
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GitlabIntegration" });
|
const logger = createLogger({ module: "gitlabIntegration" });
|
||||||
|
|
||||||
export class GitlabIntegration extends Integration implements ReleasesProviderIntegration {
|
export class GitlabIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
@@ -48,7 +48,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (releasesResponse instanceof Error) {
|
if (releasesResponse instanceof Error) {
|
||||||
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
|
logger.warn("No releases found", {
|
||||||
identifier,
|
identifier,
|
||||||
error: releasesResponse.message,
|
error: releasesResponse.message,
|
||||||
});
|
});
|
||||||
@@ -78,7 +78,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
return { success: true, data: { ...details, ...latestRelease } };
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
|
logger.warn("Failed to get releases", {
|
||||||
identifier,
|
identifier,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
const response = await api.Projects.show(identifier);
|
const response = await api.Projects.show(identifier);
|
||||||
|
|
||||||
if (response instanceof Error) {
|
if (response instanceof Error) {
|
||||||
localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, {
|
logger.warn("Failed to get details", {
|
||||||
identifier,
|
identifier,
|
||||||
error: response.message,
|
error: response.message,
|
||||||
});
|
});
|
||||||
@@ -100,7 +100,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.web_url) {
|
if (!response.web_url) {
|
||||||
localLogger.warn(`No web URL found for ${identifier} with Gitlab integration`, {
|
logger.warn("No web URL found", {
|
||||||
identifier,
|
identifier,
|
||||||
});
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -117,7 +117,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
forksCount: response.forks_count,
|
forksCount: response.forks_count,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, {
|
logger.warn("Failed to get details", {
|
||||||
identifier,
|
identifier,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import z from "zod";
|
|||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -13,13 +14,15 @@ import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-
|
|||||||
import type { CalendarEvent } from "../types";
|
import type { CalendarEvent } from "../types";
|
||||||
import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types";
|
import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "homeAssistantIntegration" });
|
||||||
|
|
||||||
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration {
|
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration {
|
||||||
public async getEntityStateAsync(entityId: string) {
|
public async getEntityStateAsync(entityId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
const response = await this.getAsync(`/api/states/${entityId}`);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.warn(`Response did not indicate success`);
|
logger.warn("Response did not indicate success");
|
||||||
return {
|
return {
|
||||||
success: false as const,
|
success: false as const,
|
||||||
error: "Response did not indicate success",
|
error: "Response did not indicate success",
|
||||||
@@ -27,7 +30,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
|||||||
}
|
}
|
||||||
return entityStateSchema.safeParseAsync(body);
|
return entityStateSchema.safeParseAsync(body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from ${this.url("/")}: ${err as string}`);
|
logger.error(new ErrorWithMetadata("Failed to fetch entity state", { entityId }, { cause: err }));
|
||||||
return {
|
return {
|
||||||
success: false as const,
|
success: false as const,
|
||||||
error: err,
|
error: err,
|
||||||
@@ -43,7 +46,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
logger.error(new ErrorWithMetadata("Failed to trigger automation", { entityId }, { cause: err }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +65,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
logger.error(new ErrorWithMetadata("Failed to toggle entity", { entityId }, { cause: err }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -9,7 +9,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
|
|||||||
import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
|
import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { releasesResponseSchema } from "./linuxserverio-schemas";
|
import { releasesResponseSchema } from "./linuxserverio-schemas";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
|
const logger = createLogger({ module: "linuxServerIOIntegration" });
|
||||||
|
|
||||||
export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration {
|
export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
@@ -27,10 +27,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
|
|||||||
private parseIdentifier(identifier: string) {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { identifier });
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${identifier} with LinuxServerIO integration`,
|
|
||||||
{ identifier },
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { owner, name };
|
return { owner, name };
|
||||||
@@ -53,7 +50,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
|
|||||||
|
|
||||||
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
|
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
|
||||||
if (!release) {
|
if (!release) {
|
||||||
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
|
logger.warn("Repository not found on provider", {
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
return { success: false, error: { code: "noMatchingVersion" } };
|
return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user