chore(release): automatic release v1.14.0

This commit is contained in:
homarr-releases[bot]
2025-04-04 19:15:16 +00:00
committed by GitHub
73 changed files with 2145 additions and 1961 deletions

View File

@@ -31,6 +31,7 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.13.1
- 1.13.0 - 1.13.0
- 1.12.0 - 1.12.0
- 1.11.0 - 1.11.0

View File

@@ -56,13 +56,13 @@
"@mantine/tiptap": "^7.17.3", "@mantine/tiptap": "^7.17.3",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.70.0", "@tanstack/react-query": "^5.71.10",
"@tanstack/react-query-devtools": "^5.70.0", "@tanstack/react-query-devtools": "^5.71.10",
"@tanstack/react-query-next-experimental": "^5.70.0", "@tanstack/react-query-next-experimental": "^5.71.10",
"@trpc/client": "^11.0.1", "@trpc/client": "^11.0.2",
"@trpc/next": "^11.0.1", "@trpc/next": "^11.0.2",
"@trpc/react-query": "^11.0.1", "@trpc/react-query": "^11.0.2",
"@trpc/server": "^11.0.1", "@trpc/server": "^11.0.2",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
@@ -81,9 +81,9 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.86.0", "sass": "^1.86.3",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.20.2", "swagger-ui-react": "^5.20.6",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
@@ -92,10 +92,10 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.13.14", "@types/node": "^22.14.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.0.12", "@types/react": "19.1.0",
"@types/react-dom": "19.0.4", "@types/react-dom": "19.1.1",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.23.0", "eslint": "^9.23.0",

View File

@@ -5,8 +5,6 @@ import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors"; import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
@@ -17,7 +15,6 @@ import type { SectionItem } from "~/app/[locale]/boards/_types";
import classes from "../sections/item.module.css"; import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
import { BoardItemMenu } from "./item-menu"; import { BoardItemMenu } from "./item-menu";
import { RestrictedWidgetContent } from "./restricted";
interface BoardItemContentProps { interface BoardItemContentProps {
item: SectionItem; item: SectionItem;
@@ -62,7 +59,6 @@ interface InnerContentProps {
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const settings = useSettings(); const settings = useSettings();
const board = useRequiredBoard(); const board = useRequiredBoard();
const { data: session } = useSession();
const [isEditMode] = useEditMode(); const [isEditMode] = useEditMode();
const Comp = loadWidgetDynamic(item.kind); const Comp = loadWidgetDynamic(item.kind);
const { definition } = widgetImports[item.kind]; const { definition } = widgetImports[item.kind];
@@ -74,16 +70,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const widgetSupportsIntegrations = const widgetSupportsIntegrations =
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1; "supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
if (
isWidgetRestricted({
definition,
user: session?.user ?? null,
check: (level) => level === "all",
})
) {
return <RestrictedWidgetContent kind={item.kind} />;
}
return ( return (
<QueryErrorResetBoundary> <QueryErrorResetBoundary>
{({ reset }) => ( {({ reset }) => (

View File

@@ -3,8 +3,6 @@ import { ActionIcon, Menu } from "@mantine/core";
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react"; import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
@@ -39,7 +37,6 @@ export const BoardItemMenu = ({
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs; const { gridstack } = useSectionContext().refs;
const settings = useSettings(); const settings = useSettings();
const { data: session } = useSession();
// Reset error boundary on next render if item has been edited // Reset error boundary on next render if item has been edited
useEffect(() => { useEffect(() => {
@@ -94,16 +91,6 @@ export const BoardItemMenu = ({
}); });
}; };
if (
isWidgetRestricted({
definition: currentDefinition,
user: session?.user ?? null,
check: (level) => level !== "none",
})
) {
return null;
}
return ( return (
<Menu withinPortal withArrow position="right-start" arrowPosition="center"> <Menu withinPortal withArrow position="right-start" arrowPosition="center">
<Menu.Target> <Menu.Target>

View File

@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core"; import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react"; import { IconSearch } from "@tabler/icons-react";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { objectEntries } from "@homarr/common"; import { objectEntries } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
@@ -17,18 +15,10 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const t = useI18n(); const t = useI18n();
const { createItem } = useItemActions(); const { createItem } = useItemActions();
const { data: session } = useSession();
const items = useMemo( const items = useMemo(
() => () =>
objectEntries(widgetImports) objectEntries(widgetImports)
.filter(([, value]) => {
return !isWidgetRestricted({
definition: value.definition,
user: session?.user ?? null,
check: (level) => level !== "none",
});
})
.map(([kind, value]) => ({ .map(([kind, value]) => ({
kind, kind,
icon: value.definition.icon, icon: value.definition.icon,
@@ -36,7 +26,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
description: t(`widget.${kind}.description`), description: t(`widget.${kind}.description`),
})) }))
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)), .sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
[t, session?.user], [t],
); );
const filteredItems = useMemo( const filteredItems = useMemo(

View File

@@ -1,28 +0,0 @@
import { Center, Group, Stack, Text } from "@mantine/core";
import { IconShield } from "@tabler/icons-react";
import type { WidgetKind } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
interface RestrictedWidgetProps {
kind: WidgetKind;
}
export const RestrictedWidgetContent = ({ kind }: RestrictedWidgetProps) => {
const tCurrentWidget = useScopedI18n(`widget.${kind}`);
const tCommonWidget = useScopedI18n("widget.common");
return (
<Center h="100%">
<Stack ta="center" gap="xs" align="center">
<Group gap="sm">
<IconShield size={16} />
<Text size="sm" fw="bold">
{tCommonWidget("restricted.title")}
</Text>
</Group>
<Text size="sm">{tCommonWidget("restricted.description", { name: tCurrentWidget("name") })}</Text>
</Stack>
</Center>
);
};

View File

@@ -0,0 +1,93 @@
"use client";
import type { PropsWithChildren } from "react";
import { Suspense, use } from "react";
import { Indicator, Menu, Text } from "@mantine/core";
import { IconBellRinging } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { useScopedI18n } from "@homarr/translation/client";
interface UpdateIndicatorProps extends PropsWithChildren {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
disabled: boolean;
}
export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => {
if (disabled || availableUpdatesPromise === undefined) {
return children;
}
return (
<Suspense fallback={children}>
<InnerUpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={disabled}>
{children}
</InnerUpdateIndicator>
</Suspense>
);
};
interface InnerUpdateIndicatorProps extends PropsWithChildren {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
disabled: boolean;
}
const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => {
const availableUpdates = use(availableUpdatesPromise);
return (
<Indicator
disabled={!availableUpdates || availableUpdates.length === 0 || disabled}
size={15}
processing
withBorder
>
{children}
</Indicator>
);
};
interface AvailableUpdatesMenuItemProps {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
}
export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => {
if (availableUpdatesPromise === undefined) {
return null;
}
return (
<Suspense fallback={null}>
<InnerAvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
</Suspense>
);
};
interface InnerAvailableUpdatesMenuItemProps {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
}
const InnerAvailableUpdatesMenuItem = ({ availableUpdatesPromise }: InnerAvailableUpdatesMenuItemProps) => {
const t = useScopedI18n("common.userAvatar.menu");
const availableUpdates = use(availableUpdatesPromise);
if (availableUpdates === undefined || availableUpdates.length === 0) {
return null;
}
const latestUpdate = availableUpdates.at(0);
if (!latestUpdate) return null;
return (
<>
<Menu.Item component={"a"} href={latestUpdate.url} target="_blank" leftSection={<IconBellRinging size="1rem" />}>
<Text fw="bold" size="sm">
{t("updateAvailable", {
countUpdates: String(availableUpdates.length),
tag: latestUpdate.tagName,
})}
</Text>
</Menu.Item>
<Menu.Divider />
</>
);
};

View File

@@ -1,21 +1,25 @@
import { Indicator, UnstyledButton } from "@mantine/core"; import { Suspense } from "react";
import { UnstyledButton } from "@mantine/core";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { CurrentUserAvatar } from "~/components/user-avatar"; import { CurrentUserAvatar } from "~/components/user-avatar";
import { UserAvatarMenu } from "~/components/user-avatar-menu"; import { UserAvatarMenu } from "~/components/user-avatar-menu";
import { UpdateIndicator } from "./update";
export const UserButton = async () => { export const UserButton = async () => {
const session = await auth(); const session = await auth();
const isAdmin = session?.user.permissions.includes("admin"); const isAdmin = session?.user.permissions.includes("admin");
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined; const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
return ( return (
<UserAvatarMenu availableUpdates={data}> <UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
<UnstyledButton> <UnstyledButton>
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder> <Suspense fallback={<CurrentUserAvatar size="md" />}>
<CurrentUserAvatar size="md" /> <UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
</Indicator> <CurrentUserAvatar size="md" />
</UpdateIndicator>
</Suspense>
</UnstyledButton> </UnstyledButton>
</UserAvatarMenu> </UserAvatarMenu>
); );

View File

@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core"; import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
import { useHotkeys, useTimeout } from "@mantine/hooks"; import { useHotkeys, useTimeout } from "@mantine/hooks";
import { import {
IconBellRinging,
IconCheck, IconCheck,
IconHome, IconHome,
IconLogin, IconLogin,
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
import { useAuthContext } from "~/app/[locale]/_client-providers/session"; import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentLanguageCombobox } from "./language/current-language-combobox"; import { CurrentLanguageCombobox } from "./language/current-language-combobox";
import { AvailableUpdatesMenuItem } from "./layout/header/update";
interface UserAvatarMenuProps { interface UserAvatarMenuProps {
children: ReactNode; children: ReactNode;
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"]; availableUpdatesPromise?: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
} }
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => { export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
const t = useScopedI18n("common.userAvatar.menu"); const t = useScopedI18n("common.userAvatar.menu");
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
useHotkeys([["mod+J", toggleColorScheme]]); useHotkeys([["mod+J", toggleColorScheme]]);
@@ -65,24 +65,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board // We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withArrow withinPortal keepMounted> <Menu width={300} withArrow withinPortal keepMounted>
<Menu.Dropdown> <Menu.Dropdown>
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && ( <AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
<>
<Menu.Item
component={"a"}
href={availableUpdates[0].url}
target="_blank"
leftSection={<IconBellRinging size="1rem" />}
>
<Text fw="bold" size="sm">
{t("updateAvailable", {
countUpdates: String(availableUpdates.length),
tag: availableUpdates[0].tagName,
})}
</Text>
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}> <Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
{colorSchemeText} {colorSchemeText}
</Menu.Item> </Menu.Item>

View File

@@ -38,13 +38,13 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"superjson": "2.2.2", "superjson": "2.2.2",
"undici": "7.6.0" "undici": "7.7.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.13.14", "@types/node": "^22.14.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.0", "@types/ws": "^8.18.1",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.2" "typescript": "^5.8.2"

View File

@@ -38,22 +38,22 @@
"@semantic-release/github": "^11.0.1", "@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.4.4", "@turbo/gen": "^2.5.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.0.9", "@vitest/ui": "^3.1.1",
"conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"semantic-release": "^24.2.3", "semantic-release": "^24.2.3",
"testcontainers": "^10.23.0", "testcontainers": "^10.24.0",
"turbo": "^2.4.4", "turbo": "^2.5.0",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9" "vitest": "^3.1.1"
}, },
"packageManager": "pnpm@10.7.0", "packageManager": "pnpm@10.7.1",
"engines": { "engines": {
"node": ">=22.14.0" "node": ">=22.14.0"
}, },
@@ -70,7 +70,7 @@
"tree-sitter-json" "tree-sitter-json"
], ],
"overrides": { "overrides": {
"proxmox-api>undici": "7.6.0" "proxmox-api>undici": "7.7.0"
}, },
"patchedDependencies": { "patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch" "pretty-print-error": "patches/pretty-print-error.patch"

View File

@@ -40,17 +40,17 @@
"@homarr/request-handler": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.0", "@kubernetes/client-node": "^1.1.1",
"@trpc/client": "^11.0.1", "@trpc/client": "^11.0.2",
"@trpc/react-query": "^11.0.1", "@trpc/react-query": "^11.0.2",
"@trpc/server": "^11.0.1", "@trpc/server": "^11.0.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.2.4", "next": "15.2.4",
"pretty-print-error": "^1.1.2", "pretty-print-error": "^1.1.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.1.3", "trpc-to-openapi": "^2.1.5",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import superjson from "superjson"; import superjson from "superjson";
import { z } from "zod"; import { z } from "zod";
import { constructBoardPermissions, isWidgetRestricted } from "@homarr/auth/shared"; import { constructBoardPermissions } from "@homarr/auth/shared";
import type { DeviceType } from "@homarr/common/server"; import type { DeviceType } from "@homarr/common/server";
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db"; import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
@@ -40,7 +40,6 @@ import { oldmarrConfigSchema } from "@homarr/old-schema";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation";
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation"; import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
import { widgetImports } from "../../../widgets/src";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access"; import { throwIfActionForbiddenAsync } from "./board/board-access";
import { generateResponsiveGridFor } from "./board/grid-algorithm"; import { generateResponsiveGridFor } from "./board/grid-algorithm";
@@ -324,13 +323,6 @@ export const boardRouter = createTRPCRouter({
} }
const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board; const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board;
const allowedBoardItems = boardItems.filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const newBoardId = createId(); const newBoardId = createId();
@@ -378,8 +370,8 @@ export const boardRouter = createTRPCRouter({
), ),
); );
const itemMap = new Map<string, string>(allowedBoardItems.map((item) => [item.id, createId()])); const itemMap = new Map<string, string>(boardItems.map((item) => [item.id, createId()]));
const itemsToInsert: InferInsertModel<typeof items>[] = allowedBoardItems.map( const itemsToInsert: InferInsertModel<typeof items>[] = boardItems.map(
({ integrations: _, layouts: _layouts, ...item }) => ({ ({ integrations: _, layouts: _layouts, ...item }) => ({
...item, ...item,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -388,7 +380,7 @@ export const boardRouter = createTRPCRouter({
}), }),
); );
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = allowedBoardItems.flatMap((item) => const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = boardItems.flatMap((item) =>
item.layouts.map( item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({ (layoutSection): InferInsertModel<typeof itemLayouts> => ({
...layoutSection, ...layoutSection,
@@ -421,7 +413,7 @@ export const boardRouter = createTRPCRouter({
) )
.then((result) => result.map((row) => row.id)); .then((result) => result.map((row) => row.id));
const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) => const itemIntegrationsToInsert = boardItems.flatMap((item) =>
item.integrations item.integrations
// Restrict integrations to only those the user has access to // Restrict integrations to only those the user has access to
.filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll) .filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll)
@@ -751,140 +743,105 @@ export const boardRouter = createTRPCRouter({
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
const sectionsToInsert = addedSections.map(
(section): InferInsertModel<typeof sections> => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
}),
);
const sectionLayoutsToInsert = addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
);
const addedItems = filterAddedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const itemsToInsert = addedItems.map(
(item): InferInsertModel<typeof items> => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
}),
);
const itemLayoutsToInsert = addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
);
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
),
);
const integrationItemsToInsert = addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
}));
const updatedItems = filterUpdatedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
),
);
const removedItems = filterRemovedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const itemIdsToRemove = removedItems.map((item) => item.id);
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
const sectionIdsToRemove = removedSections.map((section) => section.id);
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
if (sectionsToInsert.length > 0) { const addedSections = filterAddedItems(input.sections, dbBoard.sections);
await transaction.insert(schema.sections).values(sectionsToInsert);
if (addedSections.length > 0) {
await transaction.insert(schema.sections).values(
addedSections.map((section) => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
})),
);
if (addedSections.some((section) => section.kind === "dynamic")) {
await transaction.insert(schema.sectionLayouts).values(
addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof schema.sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
),
);
}
} }
if (sectionLayoutsToInsert.length > 0) { const addedItems = filterAddedItems(input.items, dbBoard.items);
await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert);
if (addedItems.length > 0) {
await transaction.insert(schema.items).values(
addedItems.map((item) => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
})),
);
await transaction.insert(schema.itemLayouts).values(
addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof schema.itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
),
);
} }
if (itemsToInsert.length > 0) { const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
await transaction.insert(schema.items).values(itemsToInsert); integrationIds.map((integrationId) => ({
} integrationId,
if (itemLayoutsToInsert.length > 0) { itemId,
await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert); })),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
if (addedIntegrationRelations.length > 0) {
await transaction.insert(schema.integrationItems).values(
addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
})),
);
} }
if (integrationItemsToInsert.length > 0) { const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
await transaction.insert(schema.integrationItems).values(integrationItemsToInsert);
}
for (const item of updatedItems) { for (const item of updatedItems) {
await transaction await transaction
@@ -915,6 +872,8 @@ export const boardRouter = createTRPCRouter({
} }
} }
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
for (const section of updatedSections) { for (const section of updatedSections) {
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
await transaction await transaction
@@ -948,6 +907,15 @@ export const boardRouter = createTRPCRouter({
} }
} }
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
for (const relation of removedIntegrationRelations) { for (const relation of removedIntegrationRelations) {
await transaction await transaction
.delete(schema.integrationItems) .delete(schema.integrationItems)
@@ -959,36 +927,134 @@ export const boardRouter = createTRPCRouter({
); );
} }
if (itemIdsToRemove.length > 0) { const removedItems = filterRemovedItems(input.items, dbBoard.items);
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove));
const itemIds = removedItems.map((item) => item.id);
if (itemIds.length > 0) {
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIds));
} }
if (sectionIdsToRemove.length > 0) { const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove)); const sectionIds = removedSections.map((section) => section.id);
if (sectionIds.length > 0) {
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds));
} }
}); });
}, },
handleSync(db) { handleSync(db) {
db.transaction((transaction) => { db.transaction((transaction) => {
if (sectionsToInsert.length > 0) { const addedSections = filterAddedItems(input.sections, dbBoard.sections);
transaction.insert(sections).values(sectionsToInsert).run();
if (addedSections.length > 0) {
transaction
.insert(sections)
.values(
addedSections.map((section) => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
})),
)
.run();
if (addedSections.some((section) => section.kind === "dynamic")) {
transaction
.insert(sectionLayouts)
.values(
addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
),
)
.run();
}
} }
if (sectionLayoutsToInsert.length > 0) { const addedItems = filterAddedItems(input.items, dbBoard.items);
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
if (addedItems.length > 0) {
transaction
.insert(items)
.values(
addedItems.map((item) => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
})),
)
.run();
transaction
.insert(itemLayouts)
.values(
addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
),
)
.run();
} }
if (itemsToInsert.length > 0) { const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
transaction.insert(items).values(itemsToInsert).run(); integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
if (addedIntegrationRelations.length > 0) {
transaction
.insert(integrationItems)
.values(
addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
})),
)
.run();
} }
if (itemLayoutsToInsert.length > 0) { const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
}
if (integrationItemsToInsert.length > 0) {
transaction.insert(integrationItems).values(integrationItemsToInsert).run();
}
for (const item of updatedItems) { for (const item of updatedItems) {
transaction transaction
@@ -1016,6 +1082,8 @@ export const boardRouter = createTRPCRouter({
} }
} }
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
for (const section of updatedSections) { for (const section of updatedSections) {
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
transaction transaction
@@ -1048,6 +1116,15 @@ export const boardRouter = createTRPCRouter({
} }
} }
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
for (const relation of removedIntegrationRelations) { for (const relation of removedIntegrationRelations) {
transaction transaction
.delete(integrationItems) .delete(integrationItems)
@@ -1060,12 +1137,18 @@ export const boardRouter = createTRPCRouter({
.run(); .run();
} }
if (itemIdsToRemove.length > 0) { const removedItems = filterRemovedItems(input.items, dbBoard.items);
transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run();
const itemIds = removedItems.map((item) => item.id);
if (itemIds.length > 0) {
transaction.delete(items).where(inArray(items.id, itemIds)).run();
} }
if (sectionIdsToRemove.length > 0) { const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run(); const sectionIds = removedSections.map((section) => section.id);
if (sectionIds.length > 0) {
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
} }
}); });
}, },
@@ -1235,7 +1318,7 @@ export const boardRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const content = await input.file.text(); const content = await input.file.text();
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session); await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
}), }),
}); });

View File

@@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({
.requiresStep("import") .requiresStep("import")
.input(importInitialOldmarrInputSchema) .input(importInitialOldmarrInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await importInitialOldmarrAsync(ctx.db, input, ctx.session); await importInitialOldmarrAsync(ctx.db, input);
await nextOnboardingStepAsync(ctx.db, undefined); await nextOnboardingStepAsync(ctx.db, undefined);
}), }),
}); });

View File

@@ -1,3 +1,2 @@
export * from "./board-permissions"; export * from "./board-permissions";
export * from "./integration-permissions"; export * from "./integration-permissions";
export * from "./widget-restriction";

View File

@@ -1,14 +0,0 @@
import type { Session } from "next-auth";
import type { WidgetDefinition } from "../../widgets/src";
import type { RestrictionLevel } from "../../widgets/src/definition";
export const isWidgetRestricted = <TDefinition extends WidgetDefinition>(props: {
definition: TDefinition;
user: Session["user"] | null;
check: (level: RestrictionLevel) => boolean;
}) => {
if (!("restrict" in props.definition)) return false;
if (props.definition.restrict === undefined) return false;
return props.check(props.definition.restrict({ user: props.user ?? null }));
};

View File

@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"undici": "7.6.0" "undici": "7.7.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,6 +1,6 @@
import fsSync from "node:fs"; import fsSync from "node:fs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { Agent } from "node:https"; import { Agent as HttpsAgent } from "node:https";
import path from "node:path"; import path from "node:path";
import { rootCertificates } from "node:tls"; import { rootCertificates } from "node:tls";
import axios from "axios"; import axios from "axios";
@@ -70,12 +70,16 @@ export const createCertificateAgentAsync = async () => {
}); });
}; };
export const createAxiosCertificateInstanceAsync = async () => { export const createHttpsAgentAsync = async () => {
const customCertificates = await loadCustomRootCertificatesAsync(); const customCertificates = await loadCustomRootCertificatesAsync();
return new HttpsAgent({
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
});
};
export const createAxiosCertificateInstanceAsync = async () => {
return axios.create({ return axios.create({
httpsAgent: new Agent({ httpsAgent: await createHttpsAgentAsync(),
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
}),
}); });
}; };

View File

@@ -27,6 +27,7 @@
"@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/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.4.7" "dotenv": "^16.4.7"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,95 @@
import { command, string } from "@drizzle-team/brocli";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { generateSecureRandomToken } from "@homarr/common/server";
import { and, count, createId, db, eq } from "@homarr/db";
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
import { usernameSchema } from "@homarr/validation";
export const recreateAdmin = command({
name: "recreate-admin",
desc: "Recreate credentials admin user if none exists anymore",
options: {
username: string("username").required().alias("u").desc("Name of the admin"),
},
// eslint-disable-next-line no-restricted-syntax
handler: async (options) => {
if (!process.env.AUTH_PROVIDERS?.toLowerCase().includes("credentials")) {
console.error("Credentials provider is not enabled");
return;
}
const result = await usernameSchema.safeParseAsync(options.username);
if (!result.success) {
console.error("Invalid username:");
console.error(result.error.errors.map((error) => `- ${error.message}`).join("\n"));
return;
}
const totalCount = await db
.select({
count: count(),
})
.from(groupPermissions)
.leftJoin(groupMembers, eq(groupMembers.groupId, groupPermissions.groupId))
.leftJoin(users, eq(users.id, groupMembers.userId))
.where(and(eq(groupPermissions.permission, "admin"), eq(users.provider, "credentials")))
.then((rows) => rows.at(0)?.count ?? 0);
if (totalCount > 0) {
console.error("Credentials admin user exists");
return;
}
const existingUser = await db.query.users.findFirst({
where: eq(users.name, result.data),
});
if (existingUser) {
console.error("User with this name already exists");
return;
}
const temporaryGroupId = createId();
const maxPosition = await getMaxGroupPositionAsync(db);
await db.insert(groups).values({
id: temporaryGroupId,
name: temporaryGroupId,
position: maxPosition + 1,
});
await db.insert(groupPermissions).values({
groupId: temporaryGroupId,
permission: "admin",
});
const salt = await createSaltAsync();
const password = generateSecureRandomToken(24);
const hashedPassword = await hashPasswordAsync(password, salt);
const userId = createId();
await db.insert(users).values({
id: userId,
name: result.data,
provider: "credentials",
password: hashedPassword,
salt,
});
await db.insert(groupMembers).values({
groupId: temporaryGroupId,
userId,
});
console.log(
"We created a new admin user for you. Please keep in mind, that the admin group of it has a temporary name. You should change it to something more meaningful.",
);
console.log(`\tUsername: ${result.data}`);
console.log(`\tPassword: ${password}`);
console.log(`\tGroup: ${temporaryGroupId}`);
console.log(""); // Empty line for better readability
},
});

View File

@@ -1,9 +1,10 @@
import { run } from "@drizzle-team/brocli"; import { run } from "@drizzle-team/brocli";
import { fixUsernames } from "./commands/fix-usernames"; import { fixUsernames } from "./commands/fix-usernames";
import { recreateAdmin } from "./commands/recreate-admin";
import { resetPassword } from "./commands/reset-password"; import { resetPassword } from "./commands/reset-password";
const commands = [resetPassword, fixUsernames]; const commands = [resetPassword, fixUsernames, recreateAdmin];
void run(commands, { void run(commands, {
name: "homarr-cli", name: "homarr-cli",

View File

@@ -33,7 +33,7 @@
"next": "15.2.4", "next": "15.2.4",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.6.0", "undici": "7.7.0",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -46,7 +46,7 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.3", "@mantine/core": "^7.17.3",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.23.0", "@testcontainers/mysql": "^10.24.0",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.9.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"drizzle-kit": "^0.30.6", "drizzle-kit": "^0.30.6",
@@ -58,7 +58,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",

View File

@@ -379,6 +379,183 @@ export type HomarrDocumentationPath =
| "/docs/1.11.0/widgets/rss" | "/docs/1.11.0/widgets/rss"
| "/docs/1.11.0/widgets/video" | "/docs/1.11.0/widgets/video"
| "/docs/1.11.0/widgets/weather" | "/docs/1.11.0/widgets/weather"
| "/docs/1.12.0/tags"
| "/docs/1.12.0/tags/active-directory"
| "/docs/1.12.0/tags/ad-guard"
| "/docs/1.12.0/tags/ad-guard-home"
| "/docs/1.12.0/tags/administration"
| "/docs/1.12.0/tags/advanced"
| "/docs/1.12.0/tags/analytics"
| "/docs/1.12.0/tags/api"
| "/docs/1.12.0/tags/apps"
| "/docs/1.12.0/tags/banner"
| "/docs/1.12.0/tags/blocking"
| "/docs/1.12.0/tags/boards"
| "/docs/1.12.0/tags/bookmark"
| "/docs/1.12.0/tags/bookmarks"
| "/docs/1.12.0/tags/caddy"
| "/docs/1.12.0/tags/certificates"
| "/docs/1.12.0/tags/checklist"
| "/docs/1.12.0/tags/code"
| "/docs/1.12.0/tags/community"
| "/docs/1.12.0/tags/configuration"
| "/docs/1.12.0/tags/connections"
| "/docs/1.12.0/tags/customization"
| "/docs/1.12.0/tags/data-sources"
| "/docs/1.12.0/tags/database"
| "/docs/1.12.0/tags/developer"
| "/docs/1.12.0/tags/development"
| "/docs/1.12.0/tags/dns"
| "/docs/1.12.0/tags/docker"
| "/docs/1.12.0/tags/donation"
| "/docs/1.12.0/tags/edit-mode"
| "/docs/1.12.0/tags/env"
| "/docs/1.12.0/tags/environment-variables"
| "/docs/1.12.0/tags/feeds"
| "/docs/1.12.0/tags/finance"
| "/docs/1.12.0/tags/getting-started"
| "/docs/1.12.0/tags/google"
| "/docs/1.12.0/tags/grafana"
| "/docs/1.12.0/tags/groups"
| "/docs/1.12.0/tags/hardware"
| "/docs/1.12.0/tags/health"
| "/docs/1.12.0/tags/help"
| "/docs/1.12.0/tags/icon-picker"
| "/docs/1.12.0/tags/icon-repositories"
| "/docs/1.12.0/tags/icons"
| "/docs/1.12.0/tags/iframe"
| "/docs/1.12.0/tags/images"
| "/docs/1.12.0/tags/installation"
| "/docs/1.12.0/tags/integrade"
| "/docs/1.12.0/tags/integration"
| "/docs/1.12.0/tags/integrations"
| "/docs/1.12.0/tags/interface"
| "/docs/1.12.0/tags/jellyserr"
| "/docs/1.12.0/tags/layout"
| "/docs/1.12.0/tags/ldap"
| "/docs/1.12.0/tags/links"
| "/docs/1.12.0/tags/lists"
| "/docs/1.12.0/tags/management"
| "/docs/1.12.0/tags/market"
| "/docs/1.12.0/tags/media"
| "/docs/1.12.0/tags/minecraft"
| "/docs/1.12.0/tags/monitoring"
| "/docs/1.12.0/tags/news"
| "/docs/1.12.0/tags/notebook"
| "/docs/1.12.0/tags/notes"
| "/docs/1.12.0/tags/oidc"
| "/docs/1.12.0/tags/open-collective"
| "/docs/1.12.0/tags/open-media-vault"
| "/docs/1.12.0/tags/overseerr"
| "/docs/1.12.0/tags/permissions"
| "/docs/1.12.0/tags/pgid"
| "/docs/1.12.0/tags/pi-hole"
| "/docs/1.12.0/tags/ping"
| "/docs/1.12.0/tags/programming"
| "/docs/1.12.0/tags/proxmox"
| "/docs/1.12.0/tags/proxy"
| "/docs/1.12.0/tags/puid"
| "/docs/1.12.0/tags/responsive"
| "/docs/1.12.0/tags/roles"
| "/docs/1.12.0/tags/rss"
| "/docs/1.12.0/tags/search"
| "/docs/1.12.0/tags/search-engines"
| "/docs/1.12.0/tags/security"
| "/docs/1.12.0/tags/self-signed"
| "/docs/1.12.0/tags/seo"
| "/docs/1.12.0/tags/server"
| "/docs/1.12.0/tags/settings"
| "/docs/1.12.0/tags/sinkhole"
| "/docs/1.12.0/tags/sso"
| "/docs/1.12.0/tags/stocks"
| "/docs/1.12.0/tags/system"
| "/docs/1.12.0/tags/table"
| "/docs/1.12.0/tags/technical-documentation"
| "/docs/1.12.0/tags/text"
| "/docs/1.12.0/tags/torrent"
| "/docs/1.12.0/tags/traefik"
| "/docs/1.12.0/tags/translations"
| "/docs/1.12.0/tags/unraid"
| "/docs/1.12.0/tags/uploads"
| "/docs/1.12.0/tags/usenet"
| "/docs/1.12.0/tags/users"
| "/docs/1.12.0/tags/variables"
| "/docs/1.12.0/tags/widgets"
| "/docs/1.12.0/advanced/command-line"
| "/docs/1.12.0/advanced/command-line/fix-usernames"
| "/docs/1.12.0/advanced/command-line/password-recovery"
| "/docs/1.12.0/advanced/development/getting-started"
| "/docs/1.12.0/advanced/development/kubernetes"
| "/docs/1.12.0/advanced/environment-variables"
| "/docs/1.12.0/advanced/icons"
| "/docs/1.12.0/advanced/keyboard-shortcuts"
| "/docs/1.12.0/advanced/proxy"
| "/docs/1.12.0/advanced/running-as-different-user"
| "/docs/1.12.0/advanced/single-sign-on"
| "/docs/1.12.0/category/advanced"
| "/docs/1.12.0/category/community"
| "/docs/1.12.0/category/developer-guides"
| "/docs/1.12.0/category/getting-started"
| "/docs/1.12.0/category/installation"
| "/docs/1.12.0/category/installation-1"
| "/docs/1.12.0/category/integrations"
| "/docs/1.12.0/category/management"
| "/docs/1.12.0/category/widgets"
| "/docs/1.12.0/community/donate"
| "/docs/1.12.0/community/faq"
| "/docs/1.12.0/community/get-in-touch"
| "/docs/1.12.0/community/license"
| "/docs/1.12.0/community/translations"
| "/docs/1.12.0/getting-started"
| "/docs/1.12.0/getting-started/after-the-installation"
| "/docs/1.12.0/getting-started/glossary"
| "/docs/1.12.0/getting-started/installation/docker"
| "/docs/1.12.0/getting-started/installation/easy-panel"
| "/docs/1.12.0/getting-started/installation/helm"
| "/docs/1.12.0/getting-started/installation/home-assistant"
| "/docs/1.12.0/getting-started/installation/portainer"
| "/docs/1.12.0/getting-started/installation/proxmox"
| "/docs/1.12.0/getting-started/installation/qnap"
| "/docs/1.12.0/getting-started/installation/railway"
| "/docs/1.12.0/getting-started/installation/saltbox"
| "/docs/1.12.0/getting-started/installation/source"
| "/docs/1.12.0/getting-started/installation/synology"
| "/docs/1.12.0/getting-started/installation/unraid"
| "/docs/1.12.0/integrations/containers"
| "/docs/1.12.0/integrations/dns"
| "/docs/1.12.0/integrations/hardware"
| "/docs/1.12.0/integrations/kubernetes"
| "/docs/1.12.0/integrations/media-requester"
| "/docs/1.12.0/integrations/media-server"
| "/docs/1.12.0/integrations/servarr"
| "/docs/1.12.0/integrations/torrent"
| "/docs/1.12.0/integrations/usenet"
| "/docs/1.12.0/management/api"
| "/docs/1.12.0/management/apps"
| "/docs/1.12.0/management/boards"
| "/docs/1.12.0/management/certificates"
| "/docs/1.12.0/management/integrations"
| "/docs/1.12.0/management/media"
| "/docs/1.12.0/management/search-engines"
| "/docs/1.12.0/management/settings"
| "/docs/1.12.0/management/users"
| "/docs/1.12.0/widgets/bookmarks"
| "/docs/1.12.0/widgets/calendar"
| "/docs/1.12.0/widgets/clock"
| "/docs/1.12.0/widgets/dns-hole"
| "/docs/1.12.0/widgets/downloads"
| "/docs/1.12.0/widgets/health-monitoring"
| "/docs/1.12.0/widgets/home-assistant"
| "/docs/1.12.0/widgets/iframe"
| "/docs/1.12.0/widgets/indexer-manager"
| "/docs/1.12.0/widgets/media-requests"
| "/docs/1.12.0/widgets/media-server"
| "/docs/1.12.0/widgets/minecraft-server-status"
| "/docs/1.12.0/widgets/notebook"
| "/docs/1.12.0/widgets/rss"
| "/docs/1.12.0/widgets/stocks"
| "/docs/1.12.0/widgets/video"
| "/docs/1.12.0/widgets/weather"
| "/docs/next/tags" | "/docs/next/tags"
| "/docs/next/tags/active-directory" | "/docs/next/tags/active-directory"
| "/docs/next/tags/ad-guard" | "/docs/next/tags/ad-guard"
@@ -671,6 +848,7 @@ export type HomarrDocumentationPath =
| "/docs/advanced/proxy" | "/docs/advanced/proxy"
| "/docs/advanced/running-as-different-user" | "/docs/advanced/running-as-different-user"
| "/docs/advanced/single-sign-on" | "/docs/advanced/single-sign-on"
| "/docs/advanced/styling"
| "/docs/category/advanced" | "/docs/category/advanced"
| "/docs/category/community" | "/docs/category/community"
| "/docs/category/developer-guides" | "/docs/category/developer-guides"
@@ -700,6 +878,7 @@ export type HomarrDocumentationPath =
| "/docs/getting-started/installation/source" | "/docs/getting-started/installation/source"
| "/docs/getting-started/installation/synology" | "/docs/getting-started/installation/synology"
| "/docs/getting-started/installation/unraid" | "/docs/getting-started/installation/unraid"
| "/docs/integrations/cloud"
| "/docs/integrations/containers" | "/docs/integrations/containers"
| "/docs/integrations/dns" | "/docs/integrations/dns"
| "/docs/integrations/hardware" | "/docs/integrations/hardware"

View File

@@ -25,13 +25,13 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.4" "dockerode": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.36", "@types/dockerode": "^3.3.37",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"typescript": "^5.8.2" "typescript": "^5.8.2"
} }

View File

@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@ctrl/deluge": "^7.1.0", "@ctrl/deluge": "^7.1.0",
"@ctrl/qbittorrent": "^9.4.0", "@ctrl/qbittorrent": "^9.5.2",
"@ctrl/transmission": "^7.2.0", "@ctrl/transmission": "^7.2.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",
@@ -39,7 +39,7 @@
"node-ical": "^0.20.1", "node-ical": "^0.20.1",
"proxmox-api": "1.1.1", "proxmox-api": "1.1.1",
"tsdav": "^2.1.3", "tsdav": "^2.1.3",
"undici": "7.6.0", "undici": "7.7.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },

View File

@@ -3,7 +3,9 @@ import objectSupport from "dayjs/plugin/objectSupport";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import * as ical from "node-ical"; import * as ical from "node-ical";
import { DAVClient } from "tsdav"; import { DAVClient } from "tsdav";
import type { RequestInit as UndiciFetchRequestInit } from "undici";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { Integration } from "../base/integration"; import { Integration } from "../base/integration";
@@ -14,12 +16,12 @@ dayjs.extend(objectSupport);
export class NextcloudIntegration extends Integration { export class NextcloudIntegration extends Integration {
public async testConnectionAsync(): Promise<void> { public async testConnectionAsync(): Promise<void> {
const client = this.createCalendarClient(); const client = await this.createCalendarClientAsync();
await client.login(); await client.login();
} }
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> { public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
const client = this.createCalendarClient(); const client = await this.createCalendarClientAsync();
await client.login(); await client.login();
const calendars = await client.fetchCalendars(); const calendars = await client.fetchCalendars();
@@ -83,7 +85,7 @@ export class NextcloudIntegration extends Integration {
}); });
} }
private createCalendarClient() { private async createCalendarClientAsync() {
return new DAVClient({ return new DAVClient({
serverUrl: this.integration.url, serverUrl: this.integration.url,
credentials: { credentials: {
@@ -92,6 +94,10 @@ export class NextcloudIntegration extends Integration {
}, },
authMethod: "Basic", authMethod: "Basic",
defaultAccountType: "caldav", defaultAccountType: "caldav",
fetchOptions: {
// We can use the undici options as the global fetch is used instead of the polyfilled.
dispatcher: await createCertificateAgentAsync(),
} satisfies UndiciFetchRequestInit as RequestInit,
}); });
} }
} }

View File

@@ -26,7 +26,6 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "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",

View File

@@ -1,12 +1,9 @@
import type { Session } from "@homarr/auth";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { createId } from "@homarr/db"; import { createId } from "@homarr/db";
import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
import { boardSizes, getBoardSizeName } from "@homarr/old-schema"; import { boardSizes, getBoardSizeName } from "@homarr/old-schema";
import { widgetImports } from "../../../../widgets/src";
import { fixSectionIssues } from "../../fix-section-issues"; import { fixSectionIssues } from "../../fix-section-issues";
import { OldHomarrImportError } from "../../import-error"; import { OldHomarrImportError } from "../../import-error";
import { mapBoard } from "../../mappers/map-board"; import { mapBoard } from "../../mappers/map-board";
@@ -21,7 +18,6 @@ import type { InitialOldmarrImportSettings } from "../../settings";
export const createBoardInsertCollection = ( export const createBoardInsertCollection = (
{ preparedApps, preparedBoards }: Omit<ReturnType<typeof prepareMultipleImports>, "preparedIntegrations">, { preparedApps, preparedBoards }: Omit<ReturnType<typeof prepareMultipleImports>, "preparedIntegrations">,
settings: InitialOldmarrImportSettings, settings: InitialOldmarrImportSettings,
session: Session | null,
) => { ) => {
const insertCollection = createDbInsertCollectionForTransaction([ const insertCollection = createDbInsertCollectionForTransaction([
"apps", "apps",
@@ -117,18 +113,10 @@ export const createBoardInsertCollection = (
layoutMapping, layoutMapping,
mappedBoard.id, mappedBoard.id,
); );
preparedItems preparedItems.forEach(({ layouts, ...item }) => {
.filter((item) => { insertCollection.items.push(item);
return !isWidgetRestricted({ insertCollection.itemLayouts.push(...layouts);
definition: widgetImports[item.kind].definition, });
user: session?.user ?? null,
check: (level) => level !== "none",
});
})
.forEach(({ layouts, ...item }) => {
insertCollection.items.push(item);
insertCollection.itemLayouts.push(...layouts);
});
logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`); logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`);
}); });

View File

@@ -1,6 +1,5 @@
import type { z } from "zod"; import type { z } from "zod";
import type { Session } from "@homarr/auth";
import { Stopwatch } from "@homarr/common"; import { Stopwatch } from "@homarr/common";
import { handleTransactionsAsync } from "@homarr/db"; import { handleTransactionsAsync } from "@homarr/db";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
@@ -17,7 +16,6 @@ import { ensureValidTokenOrThrow } from "./validate-token";
export const importInitialOldmarrAsync = async ( export const importInitialOldmarrAsync = async (
db: Database, db: Database,
input: z.infer<typeof importInitialOldmarrInputSchema>, input: z.infer<typeof importInitialOldmarrInputSchema>,
session: Session | null,
) => { ) => {
const stopwatch = new Stopwatch(); const stopwatch = new Stopwatch();
const { checksum, configs, users: importUsers } = await analyseOldmarrImportAsync(input.file); const { checksum, configs, users: importUsers } = await analyseOldmarrImportAsync(input.file);
@@ -31,7 +29,7 @@ export const importInitialOldmarrAsync = async (
logger.info("Preparing import data in insert collections for database"); logger.info("Preparing import data in insert collections for database");
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings, session); const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings);
const userInsertCollection = createUserInsertCollection(importUsers, input.token); const userInsertCollection = createUserInsertCollection(importUsers, input.token);
const integrationInsertCollection = createIntegrationInsertCollection(preparedIntegrations, input.token); const integrationInsertCollection = createIntegrationInsertCollection(preparedIntegrations, input.token);

View File

@@ -1,4 +1,3 @@
import type { Session } from "@homarr/auth";
import { handleTransactionsAsync, inArray } from "@homarr/db"; import { handleTransactionsAsync, inArray } from "@homarr/db";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { apps } from "@homarr/db/schema"; import { apps } from "@homarr/db/schema";
@@ -13,7 +12,6 @@ export const importSingleOldmarrConfigAsync = async (
db: Database, db: Database,
config: OldmarrConfig, config: OldmarrConfig,
settings: OldmarrImportConfiguration, settings: OldmarrImportConfiguration,
session: Session | null,
) => { ) => {
const { preparedApps, preparedBoards } = prepareSingleImport(config, settings); const { preparedApps, preparedBoards } = prepareSingleImport(config, settings);
const existingApps = await db.query.apps.findMany({ const existingApps = await db.query.apps.findMany({
@@ -31,7 +29,7 @@ export const importSingleOldmarrConfigAsync = async (
return app; return app;
}); });
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings, session); const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings);
await handleTransactionsAsync(db, { await handleTransactionsAsync(db, {
async handleAsync(db) { async handleAsync(db) {

View File

@@ -1,4 +1,3 @@
import type { Session } from "@homarr/auth";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import type { OldmarrConfig } from "@homarr/old-schema"; import type { OldmarrConfig } from "@homarr/old-schema";
@@ -9,7 +8,6 @@ export const importOldmarrAsync = async (
db: Database, db: Database,
old: OldmarrConfig, old: OldmarrConfig,
configuration: OldmarrImportConfiguration, configuration: OldmarrImportConfiguration,
session: Session | null,
) => { ) => {
await importSingleOldmarrConfigAsync(db, old, configuration, session); await importSingleOldmarrConfigAsync(db, old, configuration);
}; };

View File

@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import { Octokit } from "octokit"; import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser"; import { compareSemVer, isValidSemVer } from "semver-parser";
import { fetchWithTimeout } from "@homarr/common";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
@@ -12,7 +13,11 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
queryKey: "homarr-update-checker", queryKey: "homarr-update-checker",
cacheDuration: dayjs.duration(1, "hour"), cacheDuration: dayjs.duration(1, "hour"),
async requestAsync(_) { async requestAsync(_) {
const octokit = new Octokit(); const octokit = new Octokit({
request: {
fetch: fetchWithTimeout,
},
});
const releases = await octokit.rest.repos.listReleases({ const releases = await octokit.rest.repos.listReleases({
owner: "homarr-labs", owner: "homarr-labs",
repo: "homarr", repo: "homarr",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "", "name": "",

View File

@@ -677,11 +677,11 @@
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。" "description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
}, },
"createApp": { "createApp": {
"label": "", "label": "创建应用",
"description": "" "description": "创建一个具有与集成相同名称和图标的应用程序。 留空下面的输入字段使用集成URL创建应用程序。"
}, },
"appHref": { "appHref": {
"placeholder": "" "placeholder": "自定义应用URL"
} }
}, },
"action": { "action": {
@@ -981,7 +981,7 @@
}, },
"option": { "option": {
"title": { "title": {
"label": "" "label": "标题"
}, },
"borderColor": { "borderColor": {
"label": "边界颜色" "label": "边界颜色"
@@ -1428,76 +1428,76 @@
} }
}, },
"stockPrice": { "stockPrice": {
"name": "", "name": "股票价格",
"description": "", "description": "显示公司的当前股价",
"option": { "option": {
"stock": { "stock": {
"label": "" "label": "股票代码"
}, },
"timeRange": { "timeRange": {
"label": "", "label": "时间范围",
"option": { "option": {
"1d": { "1d": {
"label": "" "label": "1天"
}, },
"5d": { "5d": {
"label": "" "label": "5天"
}, },
"1mo": { "1mo": {
"label": "" "label": "1个月"
}, },
"3mo": { "3mo": {
"label": "" "label": "3 个月"
}, },
"6mo": { "6mo": {
"label": "" "label": "6 个月"
}, },
"ytd": { "ytd": {
"label": "" "label": "年初至今"
}, },
"1y": { "1y": {
"label": "" "label": "1 年"
}, },
"2y": { "2y": {
"label": "" "label": "2 年"
}, },
"5y": { "5y": {
"label": "" "label": "5 年"
}, },
"10y": { "10y": {
"label": "" "label": "10 年"
}, },
"max": { "max": {
"label": "" "label": "最大"
} }
} }
}, },
"timeInterval": { "timeInterval": {
"label": "", "label": "时间间隔",
"option": { "option": {
"5m": { "5m": {
"label": "" "label": "5 分钟"
}, },
"15m": { "15m": {
"label": "" "label": "15 分钟"
}, },
"30m": { "30m": {
"label": "" "label": "30 分钟"
}, },
"1h": { "1h": {
"label": "" "label": "1 小时"
}, },
"1d": { "1d": {
"label": "" "label": "1 天"
}, },
"5d": { "5d": {
"label": "" "label": "5 天"
}, },
"1wk": { "1wk": {
"label": "" "label": "1 周"
}, },
"1mo": { "1mo": {
"label": "" "label": "1 个月"
} }
} }
} }
@@ -1724,11 +1724,7 @@
"noIntegration": "未选择集成", "noIntegration": "未选择集成",
"noData": "没有可用的集成数据" "noData": "没有可用的集成数据"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "视频流", "name": "视频流",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Nebyla vybrána žádná integrace", "noIntegration": "Nebyla vybrána žádná integrace",
"noData": "Nejsou k dispozici žádná data o integraci" "noData": "Nejsou k dispozici žádná data o integraci"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Streamování videa", "name": "Streamování videa",

View File

@@ -981,7 +981,7 @@
}, },
"option": { "option": {
"title": { "title": {
"label": "" "label": "Titel"
}, },
"borderColor": { "borderColor": {
"label": "Kantfarve" "label": "Kantfarve"
@@ -1724,11 +1724,7 @@
"noIntegration": "Ingen integration valgt", "noIntegration": "Ingen integration valgt",
"noData": "Ingen tilgængelige integrationsdata" "noData": "Ingen tilgængelige integrationsdata"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video Stream", "name": "Video Stream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Keine Integration ausgewählt", "noIntegration": "Keine Integration ausgewählt",
"noData": "Keine Integrationsdaten verfügbar" "noData": "Keine Integrationsdaten verfügbar"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videostream", "name": "Videostream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Keine Integration ausgewählt", "noIntegration": "Keine Integration ausgewählt",
"noData": "Keine Integrationsdaten verfügbar" "noData": "Keine Integrationsdaten verfügbar"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videostream", "name": "Videostream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Ροή Βίντεο", "name": "Ροή Βίντεο",

File diff suppressed because it is too large Load Diff

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "No integration selected", "noIntegration": "No integration selected",
"noData": "No integration data available" "noData": "No integration data available"
}, },
"option": {}, "option": {}
"restricted": {
"title": "Restricted",
"description": "You don't have access to the {name} widget."
}
}, },
"video": { "video": {
"name": "Video Stream", "name": "Video Stream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video en directo", "name": "Video en directo",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "", "name": "",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Aucune intégration sélectionnée", "noIntegration": "Aucune intégration sélectionnée",
"noData": "Aucune donnée dinteraction disponible" "noData": "Aucune donnée dinteraction disponible"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Flux vidéo", "name": "Flux vidéo",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "לא נבחרה אינטגרציה", "noIntegration": "לא נבחרה אינטגרציה",
"noData": "אין נתוני אינטגרציה זמינים" "noData": "אין נתוני אינטגרציה זמינים"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "זרם וידאו", "name": "זרם וידאו",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "", "name": "",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videófolyam", "name": "Videófolyam",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Stream Video", "name": "Stream Video",

View File

@@ -3,7 +3,7 @@
"step": { "step": {
"start": { "start": {
"title": "Homarrへようこそ", "title": "Homarrへようこそ",
"subtitle": "", "subtitle": "Homarr を設定しましょう。",
"description": "", "description": "",
"action": { "action": {
"scratch": "ゼロからスタート", "scratch": "ゼロからスタート",
@@ -92,17 +92,17 @@
}, },
"finish": { "finish": {
"title": "", "title": "",
"subtitle": "", "subtitle": "準備ができています!",
"description": "", "description": "セットアップが正常に完了しました。使い始めることができます。次のアクションを選択してください:",
"action": { "action": {
"goToBoard": "", "goToBoard": "",
"createBoard": "", "createBoard": "",
"inviteUser": "", "inviteUser": "他のユーザーを招待する",
"docs": "" "docs": ""
} }
} }
}, },
"backToStart": "" "backToStart": "戻る"
}, },
"user": { "user": {
"title": "ユーザー", "title": "ユーザー",
@@ -114,7 +114,7 @@
}, },
"invite": { "invite": {
"title": "", "title": "",
"subtitle": "", "subtitle": "Homarr へようこそ!アカウントを作成してください",
"description": "" "description": ""
}, },
"init": { "init": {
@@ -133,7 +133,7 @@
"password": { "password": {
"label": "パスワード", "label": "パスワード",
"requirement": { "requirement": {
"length": "", "length": "少なくとも 8 文字以上を含む",
"lowercase": "小文字を含む", "lowercase": "小文字を含む",
"uppercase": "大文字を含む", "uppercase": "大文字を含む",
"number": "番号を含む", "number": "番号を含む",
@@ -172,13 +172,13 @@
"message": "" "message": ""
}, },
"error": { "error": {
"title": "", "title": "ログイン失敗",
"message": "" "message": "ログイン失敗"
} }
}, },
"forgotPassword": { "forgotPassword": {
"label": "", "label": "パスワードをお忘れですか?",
"description": "" "description": "管理者は、次のコマンドを使用してパスワードをリセットできます:"
} }
}, },
"register": { "register": {
@@ -865,7 +865,7 @@
"delete": "削除", "delete": "削除",
"discard": "", "discard": "",
"confirm": "確認", "confirm": "確認",
"continue": "", "continue": "次へ",
"previous": "前へ", "previous": "前へ",
"next": "次へ", "next": "次へ",
"checkoutDocs": "", "checkoutDocs": "",
@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "ビデオストリーム", "name": "ビデオストリーム",
@@ -2655,11 +2651,11 @@
"text": "" "text": ""
}, },
"integrationData": { "integrationData": {
"title": "", "title": "インテグレーション情報",
"text": "" "text": "あなたが設定したインテグレーションおよびその数量を送信します。URL、名前、その他のデータは含まれません。"
}, },
"usersData": { "usersData": {
"title": "", "title": "ユーザー情報",
"text": "" "text": ""
} }
}, },
@@ -2672,15 +2668,15 @@
}, },
"noFollow": { "noFollow": {
"title": "", "title": "",
"text": "" "text": "インデックス登録中はリンクを追跡しない。これを無効にすると、クローラーが、Homarr 上のすべてのリンクを追跡しようとします。"
}, },
"noTranslate": { "noTranslate": {
"title": "", "title": "",
"text": "" "text": "サイトの言語がユーザーの読みたい言語ではないとき、Google は検索結果に翻訳のリンクを表示します"
}, },
"noSiteLinksSearchBox": { "noSiteLinksSearchBox": {
"title": "", "title": "",
"text": "" "text": "Googleはクローラーによるリンクからサイトリンク検索ボックスを構築します。これを有効にすると Google にそのサイトリンク検索ボックスを無効にするよう求められます。"
} }
}, },
"board": { "board": {
@@ -2715,7 +2711,7 @@
"label": "", "label": "",
"options": { "options": {
"light": "", "light": "",
"dark": "" "dark": "ダーク"
} }
} }
}, },
@@ -3393,7 +3389,7 @@
"option": { "option": {
"colorScheme": { "colorScheme": {
"light": "", "light": "",
"dark": "" "dark": "ダークモードに切り替え"
}, },
"language": { "language": {
"label": "", "label": "",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "비디오 스트림", "name": "비디오 스트림",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "", "name": "",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videostraume", "name": "Videostraume",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Geen integratie geselecteerd", "noIntegration": "Geen integratie geselecteerd",
"noData": "Geen integratiegegevens beschikbaar" "noData": "Geen integratiegegevens beschikbaar"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video stream", "name": "Video stream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Ingen integrasjon valgt", "noIntegration": "Ingen integrasjon valgt",
"noData": "Ingen integrasjonsdata er tilgjengelig" "noData": "Ingen integrasjonsdata er tilgjengelig"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videostrømming", "name": "Videostrømming",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Nie wybrano integracji", "noIntegration": "Nie wybrano integracji",
"noData": "Brak danych dotyczących integracji" "noData": "Brak danych dotyczących integracji"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Strumień wideo", "name": "Strumień wideo",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Transmissão de vídeo", "name": "Transmissão de vídeo",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Stream video", "name": "Stream video",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Интеграция не выбрана", "noIntegration": "Интеграция не выбрана",
"noData": "Данные интеграции недоступны" "noData": "Данные интеграции недоступны"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Видеотрансляция", "name": "Видеотрансляция",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Nie je vybraná žiadna integrácia", "noIntegration": "Nie je vybraná žiadna integrácia",
"noData": "Nie sú k dispozícii žiadne údaje o integrácii" "noData": "Nie sú k dispozícii žiadne údaje o integrácii"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video stream", "name": "Video stream",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video tok", "name": "Video tok",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Videoström", "name": "Videoström",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Hiçbir entegrasyon seçilmedi", "noIntegration": "Hiçbir entegrasyon seçilmedi",
"noData": "Entegrasyon verisi mevcut değil" "noData": "Entegrasyon verisi mevcut değil"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Video Akışı", "name": "Video Akışı",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "Інтеграція не вибрана", "noIntegration": "Інтеграція не вибрана",
"noData": "Немає даних про інтеграцію" "noData": "Немає даних про інтеграцію"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Потокова трансляція відео", "name": "Потокова трансляція відео",

View File

@@ -1724,11 +1724,7 @@
"noIntegration": "", "noIntegration": "",
"noData": "" "noData": ""
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "Luồng video", "name": "Luồng video",

View File

@@ -981,7 +981,7 @@
}, },
"option": { "option": {
"title": { "title": {
"label": "" "label": "標題"
}, },
"borderColor": { "borderColor": {
"label": "邊框顏色" "label": "邊框顏色"
@@ -1428,76 +1428,76 @@
} }
}, },
"stockPrice": { "stockPrice": {
"name": "", "name": "股票價格",
"description": "", "description": "顯示公司當前股票價格",
"option": { "option": {
"stock": { "stock": {
"label": "" "label": "股票代碼"
}, },
"timeRange": { "timeRange": {
"label": "", "label": "時間範圍",
"option": { "option": {
"1d": { "1d": {
"label": "" "label": "單日"
}, },
"5d": { "5d": {
"label": "" "label": "五日"
}, },
"1mo": { "1mo": {
"label": "" "label": "一月"
}, },
"3mo": { "3mo": {
"label": "" "label": "三月"
}, },
"6mo": { "6mo": {
"label": "" "label": "六月"
}, },
"ytd": { "ytd": {
"label": "" "label": "今年至今"
}, },
"1y": { "1y": {
"label": "" "label": "一年"
}, },
"2y": { "2y": {
"label": "" "label": "兩年"
}, },
"5y": { "5y": {
"label": "" "label": "五年"
}, },
"10y": { "10y": {
"label": "" "label": "十年"
}, },
"max": { "max": {
"label": "" "label": "最大"
} }
} }
}, },
"timeInterval": { "timeInterval": {
"label": "", "label": "時間間隔",
"option": { "option": {
"5m": { "5m": {
"label": "" "label": "五分"
}, },
"15m": { "15m": {
"label": "" "label": "十五分"
}, },
"30m": { "30m": {
"label": "" "label": "三十分"
}, },
"1h": { "1h": {
"label": "" "label": "一小時"
}, },
"1d": { "1d": {
"label": "" "label": "一日"
}, },
"5d": { "5d": {
"label": "" "label": "五日"
}, },
"1wk": { "1wk": {
"label": "" "label": "一周"
}, },
"1mo": { "1mo": {
"label": "" "label": "一月"
} }
} }
} }
@@ -1724,11 +1724,7 @@
"noIntegration": "未選擇集成", "noIntegration": "未選擇集成",
"noData": "無可用的集成數據" "noData": "無可用的集成數據"
}, },
"option": {}, "option": {}
"restricted": {
"title": "",
"description": ""
}
}, },
"video": { "video": {
"name": "串流影音", "name": "串流影音",

View File

@@ -37,6 +37,6 @@ export {
type BoardItemIntegration, type BoardItemIntegration,
} from "./shared"; } from "./shared";
export { superRefineCertificateFile } from "./certificates"; export { superRefineCertificateFile } from "./certificates";
export { passwordRequirements } from "./user"; export { passwordRequirements, usernameSchema } from "./user";
export { supportedMediaUploadFormats } from "./media"; export { supportedMediaUploadFormats } from "./media";
export { zodEnumFromArray, zodUnionFromArray } from "./enums"; export { zodEnumFromArray, zodUnionFromArray } from "./enums";

View File

@@ -8,7 +8,7 @@ import { zodEnumFromArray } from "./enums";
import { createCustomErrorParams } from "./form/i18n"; import { createCustomErrorParams } from "./form/i18n";
// We always want the lowercase version of the username to compare it in a case-insensitive way // We always want the lowercase version of the username to compare it in a case-insensitive way
const usernameSchema = z.string().trim().toLowerCase().min(3).max(255); export const usernameSchema = z.string().trim().toLowerCase().min(3).max(255);
const regexCheck = (regex: RegExp) => (value: string) => regex.test(value); const regexCheck = (regex: RegExp) => (value: string) => regex.test(value);
export const passwordRequirements = [ export const passwordRequirements = [

View File

@@ -48,28 +48,28 @@
"@mantine/core": "^7.17.3", "@mantine/core": "^7.17.3",
"@mantine/hooks": "^7.17.3", "@mantine/hooks": "^7.17.3",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tiptap/extension-color": "2.11.6", "@tiptap/extension-color": "2.11.7",
"@tiptap/extension-highlight": "2.11.6", "@tiptap/extension-highlight": "2.11.7",
"@tiptap/extension-image": "2.11.6", "@tiptap/extension-image": "2.11.7",
"@tiptap/extension-link": "^2.11.6", "@tiptap/extension-link": "^2.11.7",
"@tiptap/extension-table": "2.11.6", "@tiptap/extension-table": "2.11.7",
"@tiptap/extension-table-cell": "2.11.6", "@tiptap/extension-table-cell": "2.11.7",
"@tiptap/extension-table-header": "2.11.6", "@tiptap/extension-table-header": "2.11.7",
"@tiptap/extension-table-row": "2.11.6", "@tiptap/extension-table-row": "2.11.7",
"@tiptap/extension-task-item": "2.11.6", "@tiptap/extension-task-item": "2.11.7",
"@tiptap/extension-task-list": "2.11.6", "@tiptap/extension-task-list": "2.11.7",
"@tiptap/extension-text-align": "2.11.6", "@tiptap/extension-text-align": "2.11.7",
"@tiptap/extension-text-style": "2.11.6", "@tiptap/extension-text-style": "2.11.7",
"@tiptap/extension-underline": "2.11.6", "@tiptap/extension-underline": "2.11.7",
"@tiptap/react": "^2.11.6", "@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.6", "@tiptap/starter-kit": "^2.11.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.2.4", "next": "15.2.4",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"recharts": "^2.15.1", "recharts": "^2.15.2",
"video.js": "^8.22.0", "video.js": "^8.22.0",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },

View File

@@ -1,7 +1,6 @@
import type { LoaderComponent } from "next/dynamic"; import type { LoaderComponent } from "next/dynamic";
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import"; import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
import type { Session } from "@homarr/auth";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import type { ServerSettings } from "@homarr/server-settings"; import type { ServerSettings } from "@homarr/server-settings";
import type { SettingsContextProps } from "@homarr/settings"; import type { SettingsContextProps } from "@homarr/settings";
@@ -44,23 +43,8 @@ export interface WidgetDefinition {
} }
> >
>; >;
/**
* Callback that returns wheter or not the widget should be available to the user.
* The widget will not be available in the widget picker and saving with a new one of this kind will not be possible.
*
* @param props contain user information
* @returns restriction type
*/
restrict?: (props: { user: Session["user"] | null }) => RestrictionLevel;
} }
/**
* none: The widget is fully available to the user.
* select: The widget is available to the user but not in the widget picker.
* all: The widget is not available to the user. As replacement a message will be shown at the widgets position.
*/
export type RestrictionLevel = "none" | "select" | "all";
export interface WidgetProps<TKind extends WidgetKind> { export interface WidgetProps<TKind extends WidgetKind> {
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>; options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
integrationIds: string[]; integrationIds: string[];

1786
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,12 +19,12 @@
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "15.2.4", "@next/eslint-plugin-next": "15.2.4",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-config-turbo": "^2.4.4", "eslint-config-turbo": "^2.5.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.28.0" "typescript-eslint": "^8.29.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",