mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 17:00:54 +01:00
chore(release): automatic release v1.14.0
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -31,6 +31,7 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.13.1
|
||||
- 1.13.0
|
||||
- 1.12.0
|
||||
- 1.11.0
|
||||
|
||||
@@ -56,13 +56,13 @@
|
||||
"@mantine/tiptap": "^7.17.3",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.70.0",
|
||||
"@tanstack/react-query-devtools": "^5.70.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.70.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/next": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"@tanstack/react-query": "^5.71.10",
|
||||
"@tanstack/react-query-devtools": "^5.71.10",
|
||||
"@tanstack/react-query-next-experimental": "^5.71.10",
|
||||
"@trpc/client": "^11.0.2",
|
||||
"@trpc/next": "^11.0.2",
|
||||
"@trpc/react-query": "^11.0.2",
|
||||
"@trpc/server": "^11.0.2",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -81,9 +81,9 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.86.0",
|
||||
"sass": "^1.86.3",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.20.2",
|
||||
"swagger-ui-react": "^5.20.6",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -92,10 +92,10 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
|
||||
@@ -5,8 +5,6 @@ import combineClasses from "clsx";
|
||||
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
|
||||
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 { useEditMode } from "@homarr/boards/edit-mode";
|
||||
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 { useItemActions } from "./item-actions";
|
||||
import { BoardItemMenu } from "./item-menu";
|
||||
import { RestrictedWidgetContent } from "./restricted";
|
||||
|
||||
interface BoardItemContentProps {
|
||||
item: SectionItem;
|
||||
@@ -62,7 +59,6 @@ interface InnerContentProps {
|
||||
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const { data: session } = useSession();
|
||||
const [isEditMode] = useEditMode();
|
||||
const Comp = loadWidgetDynamic(item.kind);
|
||||
const { definition } = widgetImports[item.kind];
|
||||
@@ -74,16 +70,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const widgetSupportsIntegrations =
|
||||
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level === "all",
|
||||
})
|
||||
) {
|
||||
return <RestrictedWidgetContent kind={item.kind} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
|
||||
@@ -3,8 +3,6 @@ import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
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 { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -39,7 +37,6 @@ export const BoardItemMenu = ({
|
||||
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
||||
const { gridstack } = useSectionContext().refs;
|
||||
const settings = useSettings();
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Reset error boundary on next render if item has been edited
|
||||
useEffect(() => {
|
||||
@@ -94,16 +91,6 @@ export const BoardItemMenu = ({
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition: currentDefinition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu withinPortal withArrow position="right-start" arrowPosition="center">
|
||||
<Menu.Target>
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
|
||||
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal } from "@homarr/modals";
|
||||
@@ -17,18 +15,10 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const t = useI18n();
|
||||
const { createItem } = useItemActions();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
objectEntries(widgetImports)
|
||||
.filter(([, value]) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: value.definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
})
|
||||
.map(([kind, value]) => ({
|
||||
kind,
|
||||
icon: value.definition.icon,
|
||||
@@ -36,7 +26,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
description: t(`widget.${kind}.description`),
|
||||
}))
|
||||
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
|
||||
[t, session?.user],
|
||||
[t],
|
||||
);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
93
apps/nextjs/src/components/layout/header/update.tsx
Normal file
93
apps/nextjs/src/components/layout/header/update.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 { auth } from "@homarr/auth/next";
|
||||
|
||||
import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||
import { UpdateIndicator } from "./update";
|
||||
|
||||
export const UserButton = async () => {
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user.permissions.includes("admin");
|
||||
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined;
|
||||
const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
|
||||
return (
|
||||
<UserAvatarMenu availableUpdates={data}>
|
||||
<UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
|
||||
<UnstyledButton>
|
||||
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</Indicator>
|
||||
<Suspense fallback={<CurrentUserAvatar size="md" />}>
|
||||
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</UpdateIndicator>
|
||||
</Suspense>
|
||||
</UnstyledButton>
|
||||
</UserAvatarMenu>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||
import {
|
||||
IconBellRinging,
|
||||
IconCheck,
|
||||
IconHome,
|
||||
IconLogin,
|
||||
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||
import { AvailableUpdatesMenuItem } from "./layout/header/update";
|
||||
|
||||
interface UserAvatarMenuProps {
|
||||
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 { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
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
|
||||
<Menu width={300} withArrow withinPortal keepMounted>
|
||||
<Menu.Dropdown>
|
||||
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
|
||||
<>
|
||||
<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 />
|
||||
</>
|
||||
)}
|
||||
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
||||
{colorSchemeText}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.6.0"
|
||||
"undici": "7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "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",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
|
||||
16
package.json
16
package.json
@@ -38,22 +38,22 @@
|
||||
"@semantic-release/github": "^11.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.4.4",
|
||||
"@turbo/gen": "^2.5.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.23.0",
|
||||
"turbo": "^2.4.4",
|
||||
"testcontainers": "^10.24.0",
|
||||
"turbo": "^2.5.0",
|
||||
"typescript": "^5.8.2",
|
||||
"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": {
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
@@ -70,7 +70,7 @@
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.6.0"
|
||||
"proxmox-api>undici": "7.7.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
|
||||
@@ -40,17 +40,17 @@
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"@kubernetes/client-node": "^1.1.1",
|
||||
"@trpc/client": "^11.0.2",
|
||||
"@trpc/react-query": "^11.0.2",
|
||||
"@trpc/server": "^11.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.2.4",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.3",
|
||||
"trpc-to-openapi": "^2.1.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
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 { Database, InferInsertModel, InferSelectModel, 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 { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
|
||||
|
||||
import { widgetImports } from "../../../widgets/src";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||
@@ -324,13 +323,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -378,8 +370,8 @@ export const boardRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
|
||||
const itemMap = new Map<string, string>(allowedBoardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = allowedBoardItems.map(
|
||||
const itemMap = new Map<string, string>(boardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = boardItems.map(
|
||||
({ integrations: _, layouts: _layouts, ...item }) => ({
|
||||
...item,
|
||||
// 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(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
...layoutSection,
|
||||
@@ -421,7 +413,7 @@ export const boardRouter = createTRPCRouter({
|
||||
)
|
||||
.then((result) => result.map((row) => row.id));
|
||||
|
||||
const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) =>
|
||||
const itemIntegrationsToInsert = boardItems.flatMap((item) =>
|
||||
item.integrations
|
||||
// Restrict integrations to only those the user has access to
|
||||
.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 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, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
if (sectionsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sections).values(sectionsToInsert);
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
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) {
|
||||
await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert);
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
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) {
|
||||
await transaction.insert(schema.items).values(itemsToInsert);
|
||||
}
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert);
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(integrationItemsToInsert);
|
||||
}
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
@@ -915,6 +872,8 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
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) {
|
||||
await transaction
|
||||
.delete(schema.integrationItems)
|
||||
@@ -959,36 +927,134 @@ export const boardRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove));
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
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) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove));
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
if (sectionsToInsert.length > 0) {
|
||||
transaction.insert(sections).values(sectionsToInsert).run();
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
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) {
|
||||
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
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) {
|
||||
transaction.insert(items).values(itemsToInsert).run();
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
transaction
|
||||
.insert(integrationItems)
|
||||
.values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
|
||||
}
|
||||
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
transaction.insert(integrationItems).values(integrationItemsToInsert).run();
|
||||
}
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
transaction
|
||||
@@ -1016,6 +1082,8 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
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) {
|
||||
transaction
|
||||
.delete(integrationItems)
|
||||
@@ -1060,12 +1137,18 @@ export const boardRouter = createTRPCRouter({
|
||||
.run();
|
||||
}
|
||||
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run();
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIds)).run();
|
||||
}
|
||||
|
||||
if (sectionIdsToRemove.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run();
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
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 }) => {
|
||||
const content = await input.file.text();
|
||||
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session);
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({
|
||||
.requiresStep("import")
|
||||
.input(importInitialOldmarrInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await importInitialOldmarrAsync(ctx.db, input, ctx.session);
|
||||
await importInitialOldmarrAsync(ctx.db, input);
|
||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./board-permissions";
|
||||
export * from "./integration-permissions";
|
||||
export * from "./widget-restriction";
|
||||
|
||||
@@ -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 }));
|
||||
};
|
||||
@@ -23,7 +23,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"undici": "7.6.0"
|
||||
"undici": "7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Agent } from "node:https";
|
||||
import { Agent as HttpsAgent } from "node:https";
|
||||
import path from "node:path";
|
||||
import { rootCertificates } from "node:tls";
|
||||
import axios from "axios";
|
||||
@@ -70,12 +70,16 @@ export const createCertificateAgentAsync = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
export const createHttpsAgentAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return new HttpsAgent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
return axios.create({
|
||||
httpsAgent: new Agent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
}),
|
||||
httpsAgent: await createHttpsAgentAsync(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
95
packages/cli/src/commands/recreate-admin.ts
Normal file
95
packages/cli/src/commands/recreate-admin.ts
Normal 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
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { run } from "@drizzle-team/brocli";
|
||||
|
||||
import { fixUsernames } from "./commands/fix-usernames";
|
||||
import { recreateAdmin } from "./commands/recreate-admin";
|
||||
import { resetPassword } from "./commands/reset-password";
|
||||
|
||||
const commands = [resetPassword, fixUsernames];
|
||||
const commands = [resetPassword, fixUsernames, recreateAdmin];
|
||||
|
||||
void run(commands, {
|
||||
name: "homarr-cli",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"undici": "7.6.0",
|
||||
"undici": "7.7.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^10.23.0",
|
||||
"@testcontainers/mysql": "^10.24.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "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",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
@@ -379,6 +379,183 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/1.11.0/widgets/rss"
|
||||
| "/docs/1.11.0/widgets/video"
|
||||
| "/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/active-directory"
|
||||
| "/docs/next/tags/ad-guard"
|
||||
@@ -671,6 +848,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/advanced/proxy"
|
||||
| "/docs/advanced/running-as-different-user"
|
||||
| "/docs/advanced/single-sign-on"
|
||||
| "/docs/advanced/styling"
|
||||
| "/docs/category/advanced"
|
||||
| "/docs/category/community"
|
||||
| "/docs/category/developer-guides"
|
||||
@@ -700,6 +878,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/getting-started/installation/source"
|
||||
| "/docs/getting-started/installation/synology"
|
||||
| "/docs/getting-started/installation/unraid"
|
||||
| "/docs/integrations/cloud"
|
||||
| "/docs/integrations/containers"
|
||||
| "/docs/integrations/dns"
|
||||
| "/docs/integrations/hardware"
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"dockerode": "^4.0.4"
|
||||
"dockerode": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.36",
|
||||
"@types/dockerode": "^3.3.37",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ctrl/deluge": "^7.1.0",
|
||||
"@ctrl/qbittorrent": "^9.4.0",
|
||||
"@ctrl/qbittorrent": "^9.5.2",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"node-ical": "^0.20.1",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.3",
|
||||
"undici": "7.6.0",
|
||||
"undici": "7.7.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,9 @@ import objectSupport from "dayjs/plugin/objectSupport";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import * as ical from "node-ical";
|
||||
import { DAVClient } from "tsdav";
|
||||
import type { RequestInit as UndiciFetchRequestInit } from "undici";
|
||||
|
||||
import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
@@ -14,12 +16,12 @@ dayjs.extend(objectSupport);
|
||||
|
||||
export class NextcloudIntegration extends Integration {
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const client = this.createCalendarClient();
|
||||
const client = await this.createCalendarClientAsync();
|
||||
await client.login();
|
||||
}
|
||||
|
||||
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
|
||||
const client = this.createCalendarClient();
|
||||
const client = await this.createCalendarClientAsync();
|
||||
await client.login();
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
@@ -83,7 +85,7 @@ export class NextcloudIntegration extends Integration {
|
||||
});
|
||||
}
|
||||
|
||||
private createCalendarClient() {
|
||||
private async createCalendarClientAsync() {
|
||||
return new DAVClient({
|
||||
serverUrl: this.integration.url,
|
||||
credentials: {
|
||||
@@ -92,6 +94,10 @@ export class NextcloudIntegration extends Integration {
|
||||
},
|
||||
authMethod: "Basic",
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { createId } from "@homarr/db";
|
||||
import { createDbInsertCollectionForTransaction } from "@homarr/db/collection";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
|
||||
import { boardSizes, getBoardSizeName } from "@homarr/old-schema";
|
||||
|
||||
import { widgetImports } from "../../../../widgets/src";
|
||||
import { fixSectionIssues } from "../../fix-section-issues";
|
||||
import { OldHomarrImportError } from "../../import-error";
|
||||
import { mapBoard } from "../../mappers/map-board";
|
||||
@@ -21,7 +18,6 @@ import type { InitialOldmarrImportSettings } from "../../settings";
|
||||
export const createBoardInsertCollection = (
|
||||
{ preparedApps, preparedBoards }: Omit<ReturnType<typeof prepareMultipleImports>, "preparedIntegrations">,
|
||||
settings: InitialOldmarrImportSettings,
|
||||
session: Session | null,
|
||||
) => {
|
||||
const insertCollection = createDbInsertCollectionForTransaction([
|
||||
"apps",
|
||||
@@ -117,18 +113,10 @@ export const createBoardInsertCollection = (
|
||||
layoutMapping,
|
||||
mappedBoard.id,
|
||||
);
|
||||
preparedItems
|
||||
.filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
})
|
||||
.forEach(({ layouts, ...item }) => {
|
||||
insertCollection.items.push(item);
|
||||
insertCollection.itemLayouts.push(...layouts);
|
||||
});
|
||||
preparedItems.forEach(({ layouts, ...item }) => {
|
||||
insertCollection.items.push(item);
|
||||
insertCollection.itemLayouts.push(...layouts);
|
||||
});
|
||||
logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { Stopwatch } from "@homarr/common";
|
||||
import { handleTransactionsAsync } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
@@ -17,7 +16,6 @@ import { ensureValidTokenOrThrow } from "./validate-token";
|
||||
export const importInitialOldmarrAsync = async (
|
||||
db: Database,
|
||||
input: z.infer<typeof importInitialOldmarrInputSchema>,
|
||||
session: Session | null,
|
||||
) => {
|
||||
const stopwatch = new Stopwatch();
|
||||
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");
|
||||
|
||||
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings, session);
|
||||
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings);
|
||||
const userInsertCollection = createUserInsertCollection(importUsers, input.token);
|
||||
const integrationInsertCollection = createIntegrationInsertCollection(preparedIntegrations, input.token);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { handleTransactionsAsync, inArray } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { apps } from "@homarr/db/schema";
|
||||
@@ -13,7 +12,6 @@ export const importSingleOldmarrConfigAsync = async (
|
||||
db: Database,
|
||||
config: OldmarrConfig,
|
||||
settings: OldmarrImportConfiguration,
|
||||
session: Session | null,
|
||||
) => {
|
||||
const { preparedApps, preparedBoards } = prepareSingleImport(config, settings);
|
||||
const existingApps = await db.query.apps.findMany({
|
||||
@@ -31,7 +29,7 @@ export const importSingleOldmarrConfigAsync = async (
|
||||
return app;
|
||||
});
|
||||
|
||||
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings, session);
|
||||
const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings);
|
||||
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(db) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Session } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
|
||||
@@ -9,7 +8,6 @@ export const importOldmarrAsync = async (
|
||||
db: Database,
|
||||
old: OldmarrConfig,
|
||||
configuration: OldmarrImportConfiguration,
|
||||
session: Session | null,
|
||||
) => {
|
||||
await importSingleOldmarrConfigAsync(db, old, configuration, session);
|
||||
await importSingleOldmarrConfigAsync(db, old, configuration);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
|
||||
import { Octokit } from "octokit";
|
||||
import { compareSemVer, isValidSemVer } from "semver-parser";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createChannelWithLatestAndEvents } from "@homarr/redis";
|
||||
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
|
||||
@@ -12,7 +13,11 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
|
||||
queryKey: "homarr-update-checker",
|
||||
cacheDuration: dayjs.duration(1, "hour"),
|
||||
async requestAsync(_) {
|
||||
const octokit = new Octokit();
|
||||
const octokit = new Octokit({
|
||||
request: {
|
||||
fetch: fetchWithTimeout,
|
||||
},
|
||||
});
|
||||
const releases = await octokit.rest.repos.listReleases({
|
||||
owner: "homarr-labs",
|
||||
repo: "homarr",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "",
|
||||
|
||||
@@ -677,11 +677,11 @@
|
||||
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
|
||||
},
|
||||
"createApp": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "创建应用",
|
||||
"description": "创建一个具有与集成相同名称和图标的应用程序。 留空下面的输入字段使用集成URL创建应用程序。"
|
||||
},
|
||||
"appHref": {
|
||||
"placeholder": ""
|
||||
"placeholder": "自定义应用URL"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -981,7 +981,7 @@
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "标题"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "边界颜色"
|
||||
@@ -1428,76 +1428,76 @@
|
||||
}
|
||||
},
|
||||
"stockPrice": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "股票价格",
|
||||
"description": "显示公司的当前股价",
|
||||
"option": {
|
||||
"stock": {
|
||||
"label": ""
|
||||
"label": "股票代码"
|
||||
},
|
||||
"timeRange": {
|
||||
"label": "",
|
||||
"label": "时间范围",
|
||||
"option": {
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "1天"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "5天"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "1个月"
|
||||
},
|
||||
"3mo": {
|
||||
"label": ""
|
||||
"label": "3 个月"
|
||||
},
|
||||
"6mo": {
|
||||
"label": ""
|
||||
"label": "6 个月"
|
||||
},
|
||||
"ytd": {
|
||||
"label": ""
|
||||
"label": "年初至今"
|
||||
},
|
||||
"1y": {
|
||||
"label": ""
|
||||
"label": "1 年"
|
||||
},
|
||||
"2y": {
|
||||
"label": ""
|
||||
"label": "2 年"
|
||||
},
|
||||
"5y": {
|
||||
"label": ""
|
||||
"label": "5 年"
|
||||
},
|
||||
"10y": {
|
||||
"label": ""
|
||||
"label": "10 年"
|
||||
},
|
||||
"max": {
|
||||
"label": ""
|
||||
"label": "最大"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeInterval": {
|
||||
"label": "",
|
||||
"label": "时间间隔",
|
||||
"option": {
|
||||
"5m": {
|
||||
"label": ""
|
||||
"label": "5 分钟"
|
||||
},
|
||||
"15m": {
|
||||
"label": ""
|
||||
"label": "15 分钟"
|
||||
},
|
||||
"30m": {
|
||||
"label": ""
|
||||
"label": "30 分钟"
|
||||
},
|
||||
"1h": {
|
||||
"label": ""
|
||||
"label": "1 小时"
|
||||
},
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "1 天"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "5 天"
|
||||
},
|
||||
"1wk": {
|
||||
"label": ""
|
||||
"label": "1 周"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "1 个月"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "未选择集成",
|
||||
"noData": "没有可用的集成数据"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "视频流",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Nebyla vybrána žádná integrace",
|
||||
"noData": "Nejsou k dispozici žádná data o integraci"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Streamování videa",
|
||||
|
||||
@@ -981,7 +981,7 @@
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "Titel"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "Kantfarve"
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Ingen integration valgt",
|
||||
"noData": "Ingen tilgængelige integrationsdata"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video Stream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Keine Integration ausgewählt",
|
||||
"noData": "Keine Integrationsdaten verfügbar"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videostream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Keine Integration ausgewählt",
|
||||
"noData": "Keine Integrationsdaten verfügbar"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videostream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Ροή Βίντεο",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "No integration selected",
|
||||
"noData": "No integration data available"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "Restricted",
|
||||
"description": "You don't have access to the {name} widget."
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video Stream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video en directo",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Aucune intégration sélectionnée",
|
||||
"noData": "Aucune donnée d’interaction disponible"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Flux vidéo",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "לא נבחרה אינטגרציה",
|
||||
"noData": "אין נתוני אינטגרציה זמינים"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "זרם וידאו",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videófolyam",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Stream Video",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "Homarrへようこそ",
|
||||
"subtitle": "",
|
||||
"subtitle": "Homarr を設定しましょう。",
|
||||
"description": "",
|
||||
"action": {
|
||||
"scratch": "ゼロからスタート",
|
||||
@@ -92,17 +92,17 @@
|
||||
},
|
||||
"finish": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"description": "",
|
||||
"subtitle": "準備ができています!",
|
||||
"description": "セットアップが正常に完了しました。使い始めることができます。次のアクションを選択してください:",
|
||||
"action": {
|
||||
"goToBoard": "",
|
||||
"createBoard": "",
|
||||
"inviteUser": "",
|
||||
"inviteUser": "他のユーザーを招待する",
|
||||
"docs": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"backToStart": ""
|
||||
"backToStart": "戻る"
|
||||
},
|
||||
"user": {
|
||||
"title": "ユーザー",
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"invite": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"subtitle": "Homarr へようこそ!アカウントを作成してください",
|
||||
"description": ""
|
||||
},
|
||||
"init": {
|
||||
@@ -133,7 +133,7 @@
|
||||
"password": {
|
||||
"label": "パスワード",
|
||||
"requirement": {
|
||||
"length": "",
|
||||
"length": "少なくとも 8 文字以上を含む",
|
||||
"lowercase": "小文字を含む",
|
||||
"uppercase": "大文字を含む",
|
||||
"number": "番号を含む",
|
||||
@@ -172,13 +172,13 @@
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "ログイン失敗",
|
||||
"message": "ログイン失敗"
|
||||
}
|
||||
},
|
||||
"forgotPassword": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "パスワードをお忘れですか?",
|
||||
"description": "管理者は、次のコマンドを使用してパスワードをリセットできます:"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
@@ -865,7 +865,7 @@
|
||||
"delete": "削除",
|
||||
"discard": "",
|
||||
"confirm": "確認",
|
||||
"continue": "",
|
||||
"continue": "次へ",
|
||||
"previous": "前へ",
|
||||
"next": "次へ",
|
||||
"checkoutDocs": "",
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "ビデオストリーム",
|
||||
@@ -2655,11 +2651,11 @@
|
||||
"text": ""
|
||||
},
|
||||
"integrationData": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "インテグレーション情報",
|
||||
"text": "あなたが設定したインテグレーション(およびその数量)を送信します。URL、名前、その他のデータは含まれません。"
|
||||
},
|
||||
"usersData": {
|
||||
"title": "",
|
||||
"title": "ユーザー情報",
|
||||
"text": ""
|
||||
}
|
||||
},
|
||||
@@ -2672,15 +2668,15 @@
|
||||
},
|
||||
"noFollow": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"text": "インデックス登録中はリンクを追跡しない。これを無効にすると、クローラーが、Homarr 上のすべてのリンクを追跡しようとします。"
|
||||
},
|
||||
"noTranslate": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"text": "サイトの言語がユーザーの読みたい言語ではないとき、Google は検索結果に翻訳のリンクを表示します"
|
||||
},
|
||||
"noSiteLinksSearchBox": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"text": "Googleはクローラーによるリンクからサイトリンク検索ボックスを構築します。これを有効にすると Google にそのサイトリンク検索ボックスを無効にするよう求められます。"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
@@ -2715,7 +2711,7 @@
|
||||
"label": "",
|
||||
"options": {
|
||||
"light": "",
|
||||
"dark": ""
|
||||
"dark": "ダーク"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3393,7 +3389,7 @@
|
||||
"option": {
|
||||
"colorScheme": {
|
||||
"light": "",
|
||||
"dark": ""
|
||||
"dark": "ダークモードに切り替え"
|
||||
},
|
||||
"language": {
|
||||
"label": "",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "비디오 스트림",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videostraume",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Geen integratie geselecteerd",
|
||||
"noData": "Geen integratiegegevens beschikbaar"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video stream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Ingen integrasjon valgt",
|
||||
"noData": "Ingen integrasjonsdata er tilgjengelig"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videostrømming",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Nie wybrano integracji",
|
||||
"noData": "Brak danych dotyczących integracji"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Strumień wideo",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Transmissão de vídeo",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Stream video",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Интеграция не выбрана",
|
||||
"noData": "Данные интеграции недоступны"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Видеотрансляция",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Nie je vybraná žiadna integrácia",
|
||||
"noData": "Nie sú k dispozícii žiadne údaje o integrácii"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video stream",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video tok",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Videoström",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Hiçbir entegrasyon seçilmedi",
|
||||
"noData": "Entegrasyon verisi mevcut değil"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Video Akışı",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "Інтеграція не вибрана",
|
||||
"noData": "Немає даних про інтеграцію"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Потокова трансляція відео",
|
||||
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "",
|
||||
"noData": ""
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "Luồng video",
|
||||
|
||||
@@ -981,7 +981,7 @@
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "標題"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "邊框顏色"
|
||||
@@ -1428,76 +1428,76 @@
|
||||
}
|
||||
},
|
||||
"stockPrice": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "股票價格",
|
||||
"description": "顯示公司當前股票價格",
|
||||
"option": {
|
||||
"stock": {
|
||||
"label": ""
|
||||
"label": "股票代碼"
|
||||
},
|
||||
"timeRange": {
|
||||
"label": "",
|
||||
"label": "時間範圍",
|
||||
"option": {
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "單日"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "五日"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "一月"
|
||||
},
|
||||
"3mo": {
|
||||
"label": ""
|
||||
"label": "三月"
|
||||
},
|
||||
"6mo": {
|
||||
"label": ""
|
||||
"label": "六月"
|
||||
},
|
||||
"ytd": {
|
||||
"label": ""
|
||||
"label": "今年至今"
|
||||
},
|
||||
"1y": {
|
||||
"label": ""
|
||||
"label": "一年"
|
||||
},
|
||||
"2y": {
|
||||
"label": ""
|
||||
"label": "兩年"
|
||||
},
|
||||
"5y": {
|
||||
"label": ""
|
||||
"label": "五年"
|
||||
},
|
||||
"10y": {
|
||||
"label": ""
|
||||
"label": "十年"
|
||||
},
|
||||
"max": {
|
||||
"label": ""
|
||||
"label": "最大"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeInterval": {
|
||||
"label": "",
|
||||
"label": "時間間隔",
|
||||
"option": {
|
||||
"5m": {
|
||||
"label": ""
|
||||
"label": "五分"
|
||||
},
|
||||
"15m": {
|
||||
"label": ""
|
||||
"label": "十五分"
|
||||
},
|
||||
"30m": {
|
||||
"label": ""
|
||||
"label": "三十分"
|
||||
},
|
||||
"1h": {
|
||||
"label": ""
|
||||
"label": "一小時"
|
||||
},
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "一日"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "五日"
|
||||
},
|
||||
"1wk": {
|
||||
"label": ""
|
||||
"label": "一周"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "一月"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1724,11 +1724,7 @@
|
||||
"noIntegration": "未選擇集成",
|
||||
"noData": "無可用的集成數據"
|
||||
},
|
||||
"option": {},
|
||||
"restricted": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
}
|
||||
"option": {}
|
||||
},
|
||||
"video": {
|
||||
"name": "串流影音",
|
||||
|
||||
@@ -37,6 +37,6 @@ export {
|
||||
type BoardItemIntegration,
|
||||
} from "./shared";
|
||||
export { superRefineCertificateFile } from "./certificates";
|
||||
export { passwordRequirements } from "./user";
|
||||
export { passwordRequirements, usernameSchema } from "./user";
|
||||
export { supportedMediaUploadFormats } from "./media";
|
||||
export { zodEnumFromArray, zodUnionFromArray } from "./enums";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { zodEnumFromArray } from "./enums";
|
||||
import { createCustomErrorParams } from "./form/i18n";
|
||||
|
||||
// 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);
|
||||
export const passwordRequirements = [
|
||||
|
||||
@@ -48,28 +48,28 @@
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tiptap/extension-color": "2.11.6",
|
||||
"@tiptap/extension-highlight": "2.11.6",
|
||||
"@tiptap/extension-image": "2.11.6",
|
||||
"@tiptap/extension-link": "^2.11.6",
|
||||
"@tiptap/extension-table": "2.11.6",
|
||||
"@tiptap/extension-table-cell": "2.11.6",
|
||||
"@tiptap/extension-table-header": "2.11.6",
|
||||
"@tiptap/extension-table-row": "2.11.6",
|
||||
"@tiptap/extension-task-item": "2.11.6",
|
||||
"@tiptap/extension-task-list": "2.11.6",
|
||||
"@tiptap/extension-text-align": "2.11.6",
|
||||
"@tiptap/extension-text-style": "2.11.6",
|
||||
"@tiptap/extension-underline": "2.11.6",
|
||||
"@tiptap/react": "^2.11.6",
|
||||
"@tiptap/starter-kit": "^2.11.6",
|
||||
"@tiptap/extension-color": "2.11.7",
|
||||
"@tiptap/extension-highlight": "2.11.7",
|
||||
"@tiptap/extension-image": "2.11.7",
|
||||
"@tiptap/extension-link": "^2.11.7",
|
||||
"@tiptap/extension-table": "2.11.7",
|
||||
"@tiptap/extension-table-cell": "2.11.7",
|
||||
"@tiptap/extension-table-header": "2.11.7",
|
||||
"@tiptap/extension-table-row": "2.11.7",
|
||||
"@tiptap/extension-task-item": "2.11.7",
|
||||
"@tiptap/extension-task-list": "2.11.7",
|
||||
"@tiptap/extension-text-align": "2.11.7",
|
||||
"@tiptap/extension-text-style": "2.11.7",
|
||||
"@tiptap/extension-underline": "2.11.7",
|
||||
"@tiptap/react": "^2.11.7",
|
||||
"@tiptap/starter-kit": "^2.11.7",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.2.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"recharts": "^2.15.1",
|
||||
"recharts": "^2.15.2",
|
||||
"video.js": "^8.22.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LoaderComponent } from "next/dynamic";
|
||||
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 { ServerSettings } from "@homarr/server-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> {
|
||||
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
|
||||
integrationIds: string[];
|
||||
|
||||
1786
pnpm-lock.yaml
generated
1786
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,12 @@
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.2.4",
|
||||
"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-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"typescript-eslint": "^8.28.0"
|
||||
"typescript-eslint": "^8.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user