chore(release): automatic release v1.15.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
|
label: Version
|
||||||
description: What version of Homarr are you running?
|
description: What version of Homarr are you running?
|
||||||
options:
|
options:
|
||||||
|
- 1.14.0
|
||||||
- 1.13.1
|
- 1.13.1
|
||||||
- 1.13.0
|
- 1.13.0
|
||||||
- 1.12.0
|
- 1.12.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm with-env next build",
|
"build": "pnpm with-env next build",
|
||||||
"clean": "git clean -xdf .next .turbo node_modules",
|
"clean": "git clean -xdf .next .turbo node_modules",
|
||||||
"dev": "pnpm with-env next dev --turbopack",
|
"dev": "pnpm with-env next dev",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"start": "pnpm with-env next start",
|
"start": "pnpm with-env next start",
|
||||||
@@ -48,21 +48,21 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^7.17.3",
|
"@mantine/colors-generator": "^7.17.4",
|
||||||
"@mantine/core": "^7.17.3",
|
"@mantine/core": "^7.17.4",
|
||||||
"@mantine/dropzone": "^7.17.3",
|
"@mantine/dropzone": "^7.17.4",
|
||||||
"@mantine/hooks": "^7.17.3",
|
"@mantine/hooks": "^7.17.4",
|
||||||
"@mantine/modals": "^7.17.3",
|
"@mantine/modals": "^7.17.4",
|
||||||
"@mantine/tiptap": "^7.17.3",
|
"@mantine/tiptap": "^7.17.4",
|
||||||
"@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.71.10",
|
"@tanstack/react-query": "^5.72.1",
|
||||||
"@tanstack/react-query-devtools": "^5.71.10",
|
"@tanstack/react-query-devtools": "^5.72.1",
|
||||||
"@tanstack/react-query-next-experimental": "^5.71.10",
|
"@tanstack/react-query-next-experimental": "^5.72.1",
|
||||||
"@trpc/client": "^11.0.2",
|
"@trpc/client": "^11.0.4",
|
||||||
"@trpc/next": "^11.0.2",
|
"@trpc/next": "^11.0.4",
|
||||||
"@trpc/react-query": "^11.0.2",
|
"@trpc/react-query": "^11.0.4",
|
||||||
"@trpc/server": "^11.0.2",
|
"@trpc/server": "^11.0.4",
|
||||||
"@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",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "15.2.4",
|
"next": "15.2.5",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.86.3",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"swagger-ui-react": "^5.20.6",
|
"swagger-ui-react": "^5.20.7",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
@@ -95,12 +95,12 @@
|
|||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.1.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.1.1",
|
"@types/react-dom": "19.1.2",
|
||||||
"@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.24.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
import { validation } from "@homarr/validation";
|
import { userRegistrationSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface RegistrationFormProps {
|
interface RegistrationFormProps {
|
||||||
invite: {
|
invite: {
|
||||||
@@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
|||||||
const t = useScopedI18n("user");
|
const t = useScopedI18n("user");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate, isPending } = clientApi.user.register.useMutation();
|
const { mutate, isPending } = clientApi.user.register.useMutation();
|
||||||
const form = useZodForm(validation.user.registration, {
|
const form = useZodForm(userRegistrationSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => {
|
const handleSubmit = (values: z.infer<typeof userRegistrationSchema>) => {
|
||||||
mutate(
|
mutate(
|
||||||
{
|
{
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { useForm } from "@homarr/form";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
providers: string[];
|
providers: string[];
|
||||||
@@ -22,7 +22,7 @@ interface LoginFormProps {
|
|||||||
callbackUrl: string;
|
callbackUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendedValidation = validation.user.signIn.extend({ provider: z.enum(["credentials", "ldap"]) });
|
const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credentials", "ldap"]) });
|
||||||
|
|
||||||
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
|
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
|
||||||
const t = useScopedI18n("user");
|
const t = useScopedI18n("user");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import type { MantineColorsTuple } from "@mantine/core";
|
import type { MantineColorsTuple } from "@mantine/core";
|
||||||
import { colorsTuple, createTheme, darken, lighten, MantineProvider } from "@mantine/core";
|
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
|
||||||
|
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import type { ColorScheme } from "@homarr/definitions";
|
import type { ColorScheme } from "@homarr/definitions";
|
||||||
@@ -24,6 +24,11 @@ export const BoardMantineProvider = ({
|
|||||||
},
|
},
|
||||||
primaryColor: "primaryColor",
|
primaryColor: "primaryColor",
|
||||||
autoContrast: true,
|
autoContrast: true,
|
||||||
|
fontSizes: {
|
||||||
|
"2xl": rem(24),
|
||||||
|
"3xl": rem(28),
|
||||||
|
"4xl": rem(36),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
|
|||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { generateColors } from "../../(content)/_theme";
|
import { generateColors } from "../../(content)/_theme";
|
||||||
@@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
|
|||||||
const progressPercentageLabel = (value: number) => `${value}%`;
|
const progressPercentageLabel = (value: number) => `${value}%`;
|
||||||
|
|
||||||
export const ColorSettingsContent = ({ board }: Props) => {
|
export const ColorSettingsContent = ({ board }: Props) => {
|
||||||
const form = useZodForm(validation.board.savePartialSettings, {
|
const form = useZodForm(boardSavePartialSettingsSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
primaryColor: board.primaryColor,
|
primaryColor: board.primaryColor,
|
||||||
secondaryColor: board.secondaryColor,
|
secondaryColor: board.secondaryColor,
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
|
import { Autocomplete, Button, Center, Grid, Group, Popover, Stack, Text } from "@mantine/core";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { IconPhotoOff } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
|
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
|
||||||
import { SelectWithDescriptionBadge } from "@homarr/ui";
|
import { SelectWithDescriptionBadge } from "@homarr/ui";
|
||||||
import { validation } from "@homarr/validation";
|
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { useSavePartialSettingsMutation } from "./_shared";
|
import { useSavePartialSettingsMutation } from "./_shared";
|
||||||
@@ -18,8 +22,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const BackgroundSettingsContent = ({ board }: Props) => {
|
export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const { data: session } = useSession();
|
||||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||||
const form = useZodForm(validation.board.savePartialSettings, {
|
const form = useZodForm(boardSavePartialSettingsSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
||||||
backgroundImageAttachment: board.backgroundImageAttachment,
|
backgroundImageAttachment: board.backgroundImageAttachment,
|
||||||
@@ -28,6 +33,16 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [debouncedSearch] = useDebouncedValue(form.values.backgroundImageUrl, 200);
|
||||||
|
const medias = clientApi.media.getPaginated.useQuery({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
includeFromAllUsers: true,
|
||||||
|
search: debouncedSearch ?? "",
|
||||||
|
});
|
||||||
|
const images = medias.data?.items.filter((media) => media.contentType.startsWith("image/")) ?? [];
|
||||||
|
const imageMap = new Map(images.map((image) => [`/api/user-medias/${image.id}`, image]));
|
||||||
|
|
||||||
const backgroundImageAttachmentData = useBackgroundOptionData(
|
const backgroundImageAttachmentData = useBackgroundOptionData(
|
||||||
"backgroundImageAttachment",
|
"backgroundImageAttachment",
|
||||||
backgroundImageAttachments,
|
backgroundImageAttachments,
|
||||||
@@ -47,8 +62,56 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col span={12}>
|
<Grid.Col span={12}>
|
||||||
<TextInput
|
<Autocomplete
|
||||||
|
leftSection={
|
||||||
|
form.values.backgroundImageUrl &&
|
||||||
|
form.values.backgroundImageUrl.trim().length >= 2 && (
|
||||||
|
<Popover width={300} withArrow>
|
||||||
|
<Popover.Target>
|
||||||
|
<Center h="100%">
|
||||||
|
<ImagePreview src={form.values.backgroundImageUrl} w={20} h={20} />
|
||||||
|
</Center>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<ImagePreview src={form.values.backgroundImageUrl} w="100%" />
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// We filter it on the server
|
||||||
|
filter={({ options }) => options}
|
||||||
label={t("board.field.backgroundImageUrl.label")}
|
label={t("board.field.backgroundImageUrl.label")}
|
||||||
|
placeholder={`${t("board.field.backgroundImageUrl.placeholder")}...`}
|
||||||
|
renderOption={({ option }) => {
|
||||||
|
const current = imageMap.get(option.value);
|
||||||
|
if (!current) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap="sm">
|
||||||
|
<ImagePreview src={option.value} w={20} h={20} />
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text size="sm">{current.name}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{option.value}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
group: t("board.field.backgroundImageUrl.group.your"),
|
||||||
|
items: images
|
||||||
|
.filter((media) => media.creatorId === session?.user.id)
|
||||||
|
.map((media) => `/api/user-medias/${media.id}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: t("board.field.backgroundImageUrl.group.other"),
|
||||||
|
items: images
|
||||||
|
.filter((media) => media.creatorId !== session?.user.id)
|
||||||
|
.map((media) => `/api/user-medias/${media.id}`),
|
||||||
|
},
|
||||||
|
]}
|
||||||
{...form.getInputProps("backgroundImageUrl")}
|
{...form.getInputProps("backgroundImageUrl")}
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -85,6 +148,21 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ImagePreviewProps {
|
||||||
|
src: string;
|
||||||
|
w: string | number;
|
||||||
|
h?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImagePreview = ({ src, w, h }: ImagePreviewProps) => {
|
||||||
|
if (!["/", "http://", "https://"].some((prefix) => src.startsWith(prefix))) {
|
||||||
|
return <IconPhotoOff size={w} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
return <img src={src} alt="preview image" style={{ width: w, height: h, objectFit: "contain" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
|
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
|
||||||
|
|
||||||
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
|
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine/core";
|
import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core";
|
||||||
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
|
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
|
||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
|
import { IconPicker } from "@homarr/forms-collection";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
@@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
|
|
||||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||||
const form = useZodForm(
|
const form = useZodForm(
|
||||||
validation.board.savePartialSettings
|
boardSavePartialSettingsSchema
|
||||||
.pick({
|
.pick({
|
||||||
pageTitle: true,
|
pageTitle: true,
|
||||||
logoImageUrl: true,
|
logoImageUrl: true,
|
||||||
@@ -52,9 +52,9 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useLogoPreview(form.values.logoImageUrl);
|
||||||
|
useFaviconPreview(form.values.faviconImageUrl);
|
||||||
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
||||||
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
|
|
||||||
const logoStatus = useLogoPreview(form.values.logoImageUrl);
|
|
||||||
|
|
||||||
// Cleanup for not applied changes of the page title and logo image URL
|
// Cleanup for not applied changes of the page title and logo image URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -94,24 +94,24 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label={t("board.field.metaTitle.label")}
|
label={t("board.field.metaTitle.label")}
|
||||||
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
|
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
|
||||||
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
|
rightSection={metaTitleStatus.isPending && <Loader size="xs" />}
|
||||||
{...form.getInputProps("metaTitle")}
|
{...form.getInputProps("metaTitle")}
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||||
<TextInput
|
<IconPicker
|
||||||
|
{...form.getInputProps("logoImageUrl")}
|
||||||
label={t("board.field.logoImageUrl.label")}
|
label={t("board.field.logoImageUrl.label")}
|
||||||
placeholder="/logo/logo.png"
|
placeholder="/logo/logo.png"
|
||||||
rightSection={<PendingOrInvalidIndicator {...logoStatus} />}
|
withAsterisk={false}
|
||||||
{...form.getInputProps("logoImageUrl")}
|
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||||
<TextInput
|
<IconPicker
|
||||||
|
{...form.getInputProps("faviconImageUrl")}
|
||||||
label={t("board.field.faviconImageUrl.label")}
|
label={t("board.field.faviconImageUrl.label")}
|
||||||
placeholder="/logo/logo.png"
|
placeholder="/logo/logo.png"
|
||||||
rightSection={<PendingOrInvalidIndicator {...faviconStatus} />}
|
withAsterisk={false}
|
||||||
{...form.getInputProps("faviconImageUrl")}
|
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -125,40 +125,16 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PendingOrInvalidIndicator = ({ isPending, isInvalid }: { isPending: boolean; isInvalid?: boolean }) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
if (isInvalid) {
|
|
||||||
return (
|
|
||||||
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
|
|
||||||
<IconAlertTriangle size="1rem" color="red" />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return <Loader size="xs" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useLogoPreview = (url: string | null) => {
|
const useLogoPreview = (url: string | null) => {
|
||||||
const { updateBoard } = useUpdateBoard();
|
const { updateBoard } = useUpdateBoard();
|
||||||
const [logoDebounced] = useDebouncedValue(url ?? "", 500);
|
const [logoDebounced] = useDebouncedValue(url ?? "", 500);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logoDebounced.includes(".") && logoDebounced.length >= 1) return;
|
|
||||||
updateBoard((previous) => ({
|
updateBoard((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null,
|
logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null,
|
||||||
}));
|
}));
|
||||||
}, [logoDebounced, updateBoard]);
|
}, [logoDebounced, updateBoard]);
|
||||||
|
|
||||||
return {
|
|
||||||
isPending: (url ?? "") !== logoDebounced,
|
|
||||||
isInvalid: logoDebounced.length >= 1 && !logoDebounced.includes("."),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useMetaTitlePreview = (title: string | null) => {
|
const useMetaTitlePreview = (title: string | null) => {
|
||||||
@@ -170,16 +146,7 @@ const useMetaTitlePreview = (title: string | null) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
|
|
||||||
const isValidUrl = (url: string) =>
|
|
||||||
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
|
|
||||||
|
|
||||||
const useFaviconPreview = (url: string | null) => {
|
const useFaviconPreview = (url: string | null) => {
|
||||||
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
||||||
useFavicon(isValidUrl(faviconDebounced) ? faviconDebounced : "");
|
useFavicon(faviconDebounced);
|
||||||
|
|
||||||
return {
|
|
||||||
isPending: (url ?? "") !== faviconDebounced,
|
|
||||||
isInvalid: faviconDebounced.length >= 1 && !isValidUrl(faviconDebounced),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { createId } from "@homarr/db/client";
|
import { createId } from "@homarr/db/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
void utils.board.getHomeBoard.invalidate();
|
void utils.board.getHomeBoard.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), {
|
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
layouts: board.layouts,
|
layouts: board.layouts,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { groupCreateSchema } from "@homarr/validation/group";
|
||||||
|
|
||||||
export const InitGroup = () => {
|
export const InitGroup = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
||||||
const form = useZodForm(validation.group.create, {
|
const form = useZodForm(groupCreateSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => {
|
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await revalidatePathActionAsync("/init");
|
await revalidatePathActionAsync("/init");
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import type { CheckboxProps } from "@homarr/form/types";
|
|||||||
import { defaultServerSettings } from "@homarr/server-settings";
|
import { defaultServerSettings } from "@homarr/server-settings";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||||
|
|
||||||
export const InitSettings = () => {
|
export const InitSettings = () => {
|
||||||
const tSection = useScopedI18n("management.page.settings.section");
|
const tSection = useScopedI18n("management.page.settings.section");
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
||||||
const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings });
|
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
||||||
|
|
||||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -30,7 +30,7 @@ export const InitSettings = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => {
|
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
||||||
await mutateAsync(values, {
|
await mutateAsync(values, {
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await revalidatePathActionAsync("/init");
|
await revalidatePathActionAsync("/init");
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
import { validation } from "@homarr/validation";
|
import { userInitSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
export const InitUserForm = () => {
|
export const InitUserForm = () => {
|
||||||
const t = useScopedI18n("user");
|
const t = useScopedI18n("user");
|
||||||
const tUser = useScopedI18n("init.step.user");
|
const tUser = useScopedI18n("init.step.user");
|
||||||
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
|
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
|
||||||
const form = useZodForm(validation.user.init, {
|
const form = useZodForm(userInitSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -74,4 +74,4 @@ export const InitUserForm = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.init>;
|
type FormType = z.infer<typeof userInitSchema>;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { AppForm } from "@homarr/forms-collection";
|
import { AppForm } from "@homarr/forms-collection";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
interface AppEditFormProps {
|
interface AppEditFormProps {
|
||||||
app: RouterOutputs["app"]["byId"];
|
app: RouterOutputs["app"]["byId"];
|
||||||
@@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(values: z.infer<typeof validation.app.manage>) => {
|
(values: z.infer<typeof appManageSchema>) => {
|
||||||
mutate({
|
mutate({
|
||||||
id: app.id,
|
id: app.id,
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
|
|||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { integrationUpdateSchema } from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||||
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
||||||
@@ -32,7 +32,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
) ?? getDefaultSecretKinds(integration.kind);
|
) ?? getDefaultSecretKinds(integration.kind);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useZodForm(validation.integration.update.omit({ id: true }), {
|
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: integration.name,
|
name: integration.name,
|
||||||
url: integration.url,
|
url: integration.url,
|
||||||
@@ -141,4 +141,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">;
|
type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;
|
||||||
|
|||||||
@@ -27,20 +27,21 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { appHrefSchema } from "@homarr/validation/app";
|
||||||
|
import { integrationCreateSchema } from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
||||||
|
|
||||||
interface NewIntegrationFormProps {
|
interface NewIntegrationFormProps {
|
||||||
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
|
searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
|
||||||
kind: IntegrationKind;
|
kind: IntegrationKind;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = validation.integration.create.omit({ kind: true }).and(
|
const formSchema = integrationCreateSchema.omit({ kind: true }).and(
|
||||||
z.object({
|
z.object({
|
||||||
createApp: z.boolean(),
|
createApp: z.boolean(),
|
||||||
appHref: validation.app.manage.shape.href,
|
appHref: appHrefSchema,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import type { IntegrationKind } from "@homarr/definitions";
|
|||||||
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import { IntegrationAvatar } from "@homarr/ui";
|
import { IntegrationAvatar } from "@homarr/ui";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { integrationCreateSchema } from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { NewIntegrationForm } from "./_integration-new-form";
|
import { NewIntegrationForm } from "./_integration-new-form";
|
||||||
|
|
||||||
interface NewIntegrationPageProps {
|
interface NewIntegrationPageProps {
|
||||||
searchParams: Promise<
|
searchParams: Promise<
|
||||||
Partial<z.infer<typeof validation.integration.create>> & {
|
Partial<z.infer<typeof integrationCreateSchema>> & {
|
||||||
kind: IntegrationKind;
|
kind: IntegrationKind;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { IconPicker } from "@homarr/forms-collection";
|
import { IconPicker } from "@homarr/forms-collection";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.searchEngine.manage>;
|
type FormType = z.infer<typeof searchEngineManageSchema>;
|
||||||
|
|
||||||
interface SearchEngineFormProps {
|
interface SearchEngineFormProps {
|
||||||
submitButtonTranslation: (t: TranslationFunction) => string;
|
submitButtonTranslation: (t: TranslationFunction) => string;
|
||||||
@@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
|
|||||||
|
|
||||||
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
|
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
|
||||||
|
|
||||||
const form = useZodForm(validation.searchEngine.manage, {
|
const form = useZodForm(searchEngineManageSchema, {
|
||||||
initialValues: initialValues ?? {
|
initialValues: initialValues ?? {
|
||||||
name: "",
|
name: "",
|
||||||
short: "",
|
short: "",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||||
|
|
||||||
import { SearchEngineForm } from "../../_form";
|
import { SearchEngineForm } from "../../_form";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(values: z.infer<typeof validation.searchEngine.manage>) => {
|
(values: z.infer<typeof searchEngineManageSchema>) => {
|
||||||
mutate({
|
mutate({
|
||||||
id: searchEngine.id,
|
id: searchEngine.id,
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||||
|
|
||||||
import { SearchEngineForm } from "../_form";
|
import { SearchEngineForm } from "../_form";
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(values: z.infer<typeof validation.searchEngine.manage>) => {
|
(values: z.infer<typeof searchEngineManageSchema>) => {
|
||||||
mutate(values);
|
mutate(values);
|
||||||
},
|
},
|
||||||
[mutate],
|
[mutate],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { createModal, useModalAction } from "@homarr/modals";
|
import { createModal, useModalAction } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { superRefineCertificateFile } from "@homarr/validation";
|
import { superRefineCertificateFile } from "@homarr/validation/certificates";
|
||||||
|
|
||||||
export const AddCertificateButton = () => {
|
export const AddCertificateButton = () => {
|
||||||
const { openModal } = useModalAction(AddCertificateModal);
|
const { openModal } = useModalAction(AddCertificateModal);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import type { Board } from "~/app/[locale]/boards/_types";
|
import type { Board } from "~/app/[locale]/boards/_types";
|
||||||
import { BoardSelect } from "~/components/board/board-select";
|
import { BoardSelect } from "~/components/board/board-select";
|
||||||
@@ -40,7 +40,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.changeHomeBoards, {
|
const form = useZodForm(userChangeHomeBoardsSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
homeBoardId: user.homeBoardId,
|
homeBoardId: user.homeBoardId,
|
||||||
mobileHomeBoardId: user.mobileHomeBoardId,
|
mobileHomeBoardId: user.mobileHomeBoardId,
|
||||||
@@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.changeHomeBoards>;
|
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface ChangeSearchPreferencesFormProps {
|
interface ChangeSearchPreferencesFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -37,7 +37,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.changeSearchPreferences, {
|
const form = useZodForm(userChangeSearchPreferencesSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
defaultSearchEngineId: user.defaultSearchEngineId,
|
defaultSearchEngineId: user.defaultSearchEngineId,
|
||||||
openInNewTab: user.openSearchInNewTab,
|
openInNewTab: user.openSearchInNewTab,
|
||||||
@@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;
|
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData);
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.firstDayOfWeek, {
|
const form = useZodForm(userFirstDayOfWeekSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
|
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
|
||||||
},
|
},
|
||||||
@@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.firstDayOfWeek>;
|
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface PingIconsEnabledProps {
|
interface PingIconsEnabledProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.pingIconsEnabled, {
|
const form = useZodForm(userPingIconsEnabledSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
pingIconsEnabled: user.pingIconsEnabled,
|
pingIconsEnabled: user.pingIconsEnabled,
|
||||||
},
|
},
|
||||||
@@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.pingIconsEnabled>;
|
type FormType = z.infer<typeof userPingIconsEnabledSchema>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { userEditProfileSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface UserProfileFormProps {
|
interface UserProfileFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -43,7 +43,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.editProfile.omit({ id: true }), {
|
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: user.name ?? "",
|
name: user.name ?? "",
|
||||||
email: user.email ?? "",
|
email: user.email ?? "",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
import { validation } from "@homarr/validation";
|
import { userChangePasswordSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
interface ChangePasswordFormProps {
|
interface ChangePasswordFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.user.changePassword, {
|
const form = useZodForm(userChangePasswordSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
/* Require previous password if the current user want's to change his password */
|
/* Require previous password if the current user want's to change his password */
|
||||||
previousPassword: session?.user.id === user.id ? "" : "_",
|
previousPassword: session?.user.id === user.id ? "" : "_",
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals";
|
|||||||
import { showErrorNotification } from "@homarr/notifications";
|
import { showErrorNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
||||||
import { validation } from "@homarr/validation";
|
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
|
||||||
import { createCustomErrorParams } from "@homarr/validation/form";
|
import { userPasswordSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { GroupSelectModal } from "~/components/access/group-select-modal";
|
import { GroupSelectModal } from "~/components/access/group-select-modal";
|
||||||
import { StepperNavigationComponent } from "./stepper-navigation";
|
import { StepperNavigationComponent } from "./stepper-navigation";
|
||||||
@@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
|||||||
const securityForm = useZodForm(
|
const securityForm = useZodForm(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
password: validation.user.password,
|
password: userPasswordSchema,
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { groupUpdateSchema } from "@homarr/validation/group";
|
||||||
|
|
||||||
interface RenameGroupFormProps {
|
interface RenameGroupFormProps {
|
||||||
group: {
|
group: {
|
||||||
@@ -21,7 +21,7 @@ interface RenameGroupFormProps {
|
|||||||
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
|
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
||||||
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
const form = useZodForm(groupUpdateSchema.pick({ name: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: group.name,
|
name: group.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { groupSettingsSchema } from "@homarr/validation/group";
|
||||||
|
|
||||||
import { BoardSelect } from "~/components/board/board-select";
|
import { BoardSelect } from "~/components/board/board-select";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ interface GroupHomeBoardsProps {
|
|||||||
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
|
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId });
|
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId });
|
||||||
const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
|
const form = useZodForm(groupSettingsSchema.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
homeBoardId,
|
homeBoardId,
|
||||||
mobileHomeBoardId,
|
mobileHomeBoardId,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals";
|
|||||||
import { showSuccessNotification } from "@homarr/notifications";
|
import { showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useSettings } from "@homarr/settings";
|
import { useSettings } from "@homarr/settings";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
||||||
import { WidgetError } from "@homarr/widgets/errors";
|
import { WidgetError } from "@homarr/widgets/errors";
|
||||||
import { WidgetEditModal } from "@homarr/widgets/modals";
|
import { WidgetEditModal } from "@homarr/widgets/modals";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
|
|
||||||
import type { CreateItemInput } from "./actions/create-item";
|
import type { CreateItemInput } from "./actions/create-item";
|
||||||
import { createItemCallback } from "./actions/create-item";
|
import { createItemCallback } from "./actions/create-item";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { boardRenameSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,7 +26,7 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
|
|||||||
void utils.board.getHomeBoard.invalidate();
|
void utils.board.getHomeBoard.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
const form = useZodForm(boardRenameSchema.omit({ id: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: innerProps.previousName,
|
name: innerProps.previousName,
|
||||||
},
|
},
|
||||||
@@ -66,4 +66,4 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
|
|||||||
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
|
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">;
|
type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||||
import type { dynamicSectionOptionsSchema } from "@homarr/validation";
|
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
import { addDynamicSectionCallback } from "./actions/add-dynamic-section";
|
import { addDynamicSectionCallback } from "./actions/add-dynamic-section";
|
||||||
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";
|
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { z } from "zod";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { dynamicSectionOptionsSchema } from "@homarr/validation";
|
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
value: z.infer<typeof dynamicSectionOptionsSchema>;
|
value: z.infer<typeof dynamicSectionOptionsSchema>;
|
||||||
|
|||||||
@@ -46,9 +46,9 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tsx": "4.19.3",
|
"tsx": "4.19.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"@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.1",
|
"@types/ws": "^8.18.1",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
client_max_body_size 32M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +47,13 @@
|
|||||||
"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.24.0",
|
"testcontainers": "^10.24.1",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.5.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.1.1"
|
"vitest": "^3.1.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.7.1",
|
"packageManager": "pnpm@10.8.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.14.0"
|
"node": ">=22.14.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,12 @@
|
|||||||
"@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.1",
|
"@kubernetes/client-node": "^1.1.2",
|
||||||
"@trpc/client": "^11.0.2",
|
"@trpc/client": "^11.0.4",
|
||||||
"@trpc/react-query": "^11.0.2",
|
"@trpc/react-query": "^11.0.4",
|
||||||
"@trpc/server": "^11.0.2",
|
"@trpc/server": "^11.0.4",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.5",
|
||||||
"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",
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
"@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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db";
|
|||||||
import { apps } from "@homarr/db/schema";
|
import { apps } from "@homarr/db/schema";
|
||||||
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
||||||
import { getIconForName } from "@homarr/icons";
|
import { getIconForName } from "@homarr/icons";
|
||||||
import { validation } from "@homarr/validation";
|
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
|
||||||
|
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||||
|
|
||||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
@@ -15,7 +16,7 @@ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@mas
|
|||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
getPaginated: protectedProcedure
|
getPaginated: protectedProcedure
|
||||||
.input(validation.common.paginated)
|
.input(paginatedSchema)
|
||||||
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
|
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
|
||||||
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
|
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
@@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
byId: publicProcedure
|
byId: publicProcedure
|
||||||
.input(validation.common.byId)
|
.input(byIdSchema)
|
||||||
.output(selectAppSchema)
|
.output(selectAppSchema)
|
||||||
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
@@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("app-create")
|
.requiresPermission("app-create")
|
||||||
.input(validation.app.manage)
|
.input(appManageSchema)
|
||||||
.output(z.object({ appId: z.string() }))
|
.output(z.object({ appId: z.string() }))
|
||||||
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
|
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
createMany: permissionRequiredProcedure
|
createMany: permissionRequiredProcedure
|
||||||
.requiresPermission("app-create")
|
.requiresPermission("app-create")
|
||||||
.input(validation.app.createMany)
|
.input(appCreateManySchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db.insert(apps).values(
|
await ctx.db.insert(apps).values(
|
||||||
@@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
update: permissionRequiredProcedure
|
update: permissionRequiredProcedure
|
||||||
.requiresPermission("app-modify-all")
|
.requiresPermission("app-modify-all")
|
||||||
.input(convertIntersectionToZodObject(validation.app.edit))
|
.input(convertIntersectionToZodObject(appEditSchema))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
.requiresPermission("app-full-all")
|
.requiresPermission("app-full-all")
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||||
.input(validation.common.byId)
|
.input(byIdSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db.delete(apps).where(eq(apps.id, input.id));
|
await ctx.db.delete(apps).where(eq(apps.id, input.id));
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -37,8 +37,21 @@ import {
|
|||||||
import { importOldmarrAsync } from "@homarr/old-import";
|
import { importOldmarrAsync } from "@homarr/old-import";
|
||||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||||
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import {
|
||||||
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
|
boardByNameSchema,
|
||||||
|
boardChangeVisibilitySchema,
|
||||||
|
boardCreateSchema,
|
||||||
|
boardDuplicateSchema,
|
||||||
|
boardRenameSchema,
|
||||||
|
boardSaveLayoutsSchema,
|
||||||
|
boardSavePartialSettingsSchema,
|
||||||
|
boardSavePermissionsSchema,
|
||||||
|
boardSaveSchema,
|
||||||
|
} from "@homarr/validation/board";
|
||||||
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
|
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||||
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
|
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
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";
|
||||||
@@ -247,7 +260,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
createBoard: permissionRequiredProcedure
|
createBoard: permissionRequiredProcedure
|
||||||
.requiresPermission("board-create")
|
.requiresPermission("board-create")
|
||||||
.input(validation.board.create)
|
.input(boardCreateSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const boardId = createId();
|
const boardId = createId();
|
||||||
|
|
||||||
@@ -291,7 +304,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
duplicateBoard: permissionRequiredProcedure
|
duplicateBoard: permissionRequiredProcedure
|
||||||
.requiresPermission("board-create")
|
.requiresPermission("board-create")
|
||||||
.input(validation.board.duplicate)
|
.input(boardDuplicateSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
||||||
await noBoardWithSimilarNameAsync(ctx.db, input.name);
|
await noBoardWithSimilarNameAsync(ctx.db, input.name);
|
||||||
@@ -506,34 +519,32 @@ export const boardRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => {
|
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
|
|
||||||
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
||||||
|
|
||||||
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
|
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
changeBoardVisibility: protectedProcedure
|
changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
|
||||||
.input(validation.board.changeVisibility)
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
.mutation(async ({ ctx, input }) => {
|
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
|
||||||
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
input.visibility !== "public" &&
|
input.visibility !== "public" &&
|
||||||
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Cannot make home board private",
|
message: "Cannot make home board private",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(boards)
|
.update(boards)
|
||||||
.set({ isPublic: input.visibility === "public" })
|
.set({ isPublic: input.visibility === "public" })
|
||||||
.where(eq(boards.id, input.id));
|
.where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
|
|
||||||
@@ -572,13 +583,13 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
}),
|
}),
|
||||||
getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => {
|
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||||
|
|
||||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||||
}),
|
}),
|
||||||
saveLayouts: protectedProcedure.input(validation.board.saveLayouts).mutation(async ({ ctx, input }) => {
|
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
|
|
||||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||||
@@ -704,7 +715,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
savePartialBoardSettings: protectedProcedure
|
savePartialBoardSettings: protectedProcedure
|
||||||
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() })))
|
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
|
|
||||||
@@ -738,7 +749,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(boards.id, input.id));
|
.where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
|
saveBoard: protectedProcedure.input(boardSaveSchema).mutation(async ({ input, ctx }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
|
|
||||||
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);
|
||||||
@@ -1154,8 +1165,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
||||||
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
|
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
|
|
||||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||||
@@ -1226,92 +1236,86 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
saveUserBoardPermissions: protectedProcedure
|
saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
||||||
.input(validation.board.savePermissions)
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
|
||||||
|
|
||||||
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) => {
|
||||||
await transaction
|
await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
|
||||||
.delete(schema.boardUserPermissions)
|
if (input.permissions.length === 0) {
|
||||||
.where(eq(boardUserPermissions.boardId, input.entityId));
|
return;
|
||||||
if (input.permissions.length === 0) {
|
}
|
||||||
return;
|
await transaction.insert(schema.boardUserPermissions).values(
|
||||||
}
|
input.permissions.map((permission) => ({
|
||||||
await transaction.insert(schema.boardUserPermissions).values(
|
userId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
boardId: input.entityId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(boardUserPermissions)
|
||||||
|
.values(
|
||||||
input.permissions.map((permission) => ({
|
input.permissions.map((permission) => ({
|
||||||
userId: permission.principalId,
|
userId: permission.principalId,
|
||||||
permission: permission.permission,
|
permission: permission.permission,
|
||||||
boardId: input.entityId,
|
boardId: input.entityId,
|
||||||
})),
|
})),
|
||||||
);
|
)
|
||||||
});
|
.run();
|
||||||
},
|
});
|
||||||
handleSync(db) {
|
},
|
||||||
db.transaction((transaction) => {
|
});
|
||||||
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
}),
|
||||||
if (input.permissions.length === 0) {
|
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
||||||
return;
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||||
}
|
|
||||||
transaction
|
|
||||||
.insert(boardUserPermissions)
|
|
||||||
.values(
|
|
||||||
input.permissions.map((permission) => ({
|
|
||||||
userId: permission.principalId,
|
|
||||||
permission: permission.permission,
|
|
||||||
boardId: input.entityId,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
saveGroupBoardPermissions: protectedProcedure
|
|
||||||
.input(validation.board.savePermissions)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
|
||||||
|
|
||||||
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) => {
|
||||||
await transaction
|
await transaction
|
||||||
.delete(schema.boardGroupPermissions)
|
.delete(schema.boardGroupPermissions)
|
||||||
.where(eq(boardGroupPermissions.boardId, input.entityId));
|
.where(eq(boardGroupPermissions.boardId, input.entityId));
|
||||||
if (input.permissions.length === 0) {
|
if (input.permissions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await transaction.insert(schema.boardGroupPermissions).values(
|
await transaction.insert(schema.boardGroupPermissions).values(
|
||||||
|
input.permissions.map((permission) => ({
|
||||||
|
groupId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
boardId: input.entityId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(boardGroupPermissions)
|
||||||
|
.values(
|
||||||
input.permissions.map((permission) => ({
|
input.permissions.map((permission) => ({
|
||||||
groupId: permission.principalId,
|
groupId: permission.principalId,
|
||||||
permission: permission.permission,
|
permission: permission.permission,
|
||||||
boardId: input.entityId,
|
boardId: input.entityId,
|
||||||
})),
|
})),
|
||||||
);
|
)
|
||||||
});
|
.run();
|
||||||
},
|
});
|
||||||
handleSync(db) {
|
},
|
||||||
db.transaction((transaction) => {
|
});
|
||||||
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
}),
|
||||||
if (input.permissions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
transaction
|
|
||||||
.insert(boardGroupPermissions)
|
|
||||||
.values(
|
|
||||||
input.permissions.map((permission) => ({
|
|
||||||
groupId: permission.principalId,
|
|
||||||
permission: permission.permission,
|
|
||||||
boardId: input.entityId,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
importOldmarrConfig: permissionRequiredProcedure
|
importOldmarrConfig: permissionRequiredProcedure
|
||||||
.requiresPermission("board-create")
|
.requiresPermission("board-create")
|
||||||
.input(importJsonFileSchema)
|
.input(importJsonFileSchema)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
import { zfd } from "zod-form-data";
|
import { zfd } from "zod-form-data";
|
||||||
|
|
||||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||||
import { superRefineCertificateFile, validation } from "@homarr/validation";
|
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
removeCertificate: permissionRequiredProcedure
|
removeCertificate: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(z.object({ fileName: validation.certificates.validFileNameSchema }))
|
.input(z.object({ fileName: certificateValidFileNameSchema }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await removeCustomRootCertificateAsync(input.fileName);
|
await removeCustomRootCertificateAsync(input.fileName);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d
|
|||||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||||
import { everyoneGroup } from "@homarr/definitions";
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { validation } from "@homarr/validation";
|
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||||
|
import {
|
||||||
|
groupCreateSchema,
|
||||||
|
groupSavePartialSettingsSchema,
|
||||||
|
groupSavePermissionsSchema,
|
||||||
|
groupSavePositionsSchema,
|
||||||
|
groupUpdateSchema,
|
||||||
|
groupUserSchema,
|
||||||
|
} from "@homarr/validation/group";
|
||||||
|
|
||||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||||
@@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getPaginated: permissionRequiredProcedure
|
getPaginated: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.common.paginated)
|
.input(paginatedSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||||
const groupCount = await ctx.db.$count(groups, whereQuery);
|
const groupCount = await ctx.db.$count(groups, whereQuery);
|
||||||
@@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
getById: permissionRequiredProcedure
|
getById: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.common.byId)
|
.input(byIdSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const group = await ctx.db.query.groups.findFirst({
|
const group = await ctx.db.query.groups.findFirst({
|
||||||
where: eq(groups.id, input.id),
|
where: eq(groups.id, input.id),
|
||||||
@@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
createInitialExternalGroup: onboardingProcedure
|
createInitialExternalGroup: onboardingProcedure
|
||||||
.requiresStep("group")
|
.requiresStep("group")
|
||||||
.input(validation.group.create)
|
.input(groupCreateSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||||
|
|
||||||
@@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
createGroup: permissionRequiredProcedure
|
createGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.create)
|
.input(groupCreateSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||||
|
|
||||||
@@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
updateGroup: permissionRequiredProcedure
|
updateGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.update)
|
.input(groupUpdateSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
@@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
savePartialSettings: permissionRequiredProcedure
|
savePartialSettings: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.savePartialSettings)
|
.input(groupSavePartialSettingsSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
|
|
||||||
@@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
savePositions: permissionRequiredProcedure
|
savePositions: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.savePositions)
|
.input(groupSavePositionsSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
|
const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
|
||||||
|
|
||||||
@@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
savePermissions: permissionRequiredProcedure
|
savePermissions: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.savePermissions)
|
.input(groupSavePermissionsSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
|
|
||||||
@@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
transferOwnership: permissionRequiredProcedure
|
transferOwnership: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.groupUser)
|
.input(groupUserSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
@@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
deleteGroup: permissionRequiredProcedure
|
deleteGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.common.byId)
|
.input(byIdSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
@@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
addMember: permissionRequiredProcedure
|
addMember: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.groupUser)
|
.input(groupUserSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
@@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
removeMember: permissionRequiredProcedure
|
removeMember: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.groupUser)
|
.input(groupUserSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { and, like } from "@homarr/db";
|
import { and, like } from "@homarr/db";
|
||||||
import { icons } from "@homarr/db/schema";
|
import { icons } from "@homarr/db/schema";
|
||||||
import { validation } from "@homarr/validation";
|
import { iconsFindSchema } from "@homarr/validation/icons";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
export const iconsRouter = createTRPCRouter({
|
export const iconsRouter = createTRPCRouter({
|
||||||
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => {
|
findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => {
|
||||||
return {
|
return {
|
||||||
icons: await ctx.db.query.iconRepositories.findMany({
|
icons: await ctx.db.query.iconRepositories.findMany({
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import {
|
|||||||
integrationSecretKindObject,
|
integrationSecretKindObject,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { validation } from "@homarr/validation";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
|
import {
|
||||||
|
integrationCreateSchema,
|
||||||
|
integrationSavePermissionsSchema,
|
||||||
|
integrationUpdateSchema,
|
||||||
|
} from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
@@ -141,7 +146,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => {
|
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||||
const integration = await ctx.db.query.integrations.findFirst({
|
const integration = await ctx.db.query.integrations.findFirst({
|
||||||
where: eq(integrations.id, input.id),
|
where: eq(integrations.id, input.id),
|
||||||
@@ -178,7 +183,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("integration-create")
|
.requiresPermission("integration-create")
|
||||||
.input(validation.integration.create)
|
.input(integrationCreateSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await testConnectionAsync({
|
await testConnectionAsync({
|
||||||
id: "new",
|
id: "new",
|
||||||
@@ -221,7 +226,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
|
update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||||
|
|
||||||
const integration = await ctx.db.query.integrations.findFirst({
|
const integration = await ctx.db.query.integrations.findFirst({
|
||||||
@@ -282,7 +287,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => {
|
delete: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||||
|
|
||||||
const integration = await ctx.db.query.integrations.findFirst({
|
const integration = await ctx.db.query.integrations.findFirst({
|
||||||
@@ -298,7 +303,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
||||||
}),
|
}),
|
||||||
getIntegrationPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
|
getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||||
|
|
||||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||||
@@ -370,7 +375,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
saveUserIntegrationPermissions: protectedProcedure
|
saveUserIntegrationPermissions: protectedProcedure
|
||||||
.input(validation.integration.savePermissions)
|
.input(integrationSavePermissionsSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||||
|
|
||||||
@@ -416,7 +421,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
saveGroupIntegrationPermissions: protectedProcedure
|
saveGroupIntegrationPermissions: protectedProcedure
|
||||||
.input(validation.integration.savePermissions)
|
.input(integrationSavePermissionsSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,42 @@
|
|||||||
import type { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fetchWithTimeout } from "@homarr/common";
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
import { validation } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
|
const citySchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
country: z.string().optional(),
|
||||||
|
country_code: z.string().optional(),
|
||||||
|
latitude: z.number(),
|
||||||
|
longitude: z.number(),
|
||||||
|
population: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const locationSearchCityInput = z.object({
|
||||||
|
query: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const locationSearchCityOutput = z
|
||||||
|
.object({
|
||||||
|
results: z.array(citySchema),
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
generationtime_ms: z.number(),
|
||||||
|
})
|
||||||
|
.refine((data) => Object.keys(data).length === 1, { message: "Invalid response" })
|
||||||
|
.transform(() => ({ results: [] })), // We fallback to empty array if no results
|
||||||
|
);
|
||||||
|
|
||||||
export const locationRouter = createTRPCRouter({
|
export const locationRouter = createTRPCRouter({
|
||||||
searchCity: publicProcedure
|
searchCity: publicProcedure
|
||||||
.input(validation.location.searchCity.input)
|
.input(locationSearchCityInput)
|
||||||
.output(validation.location.searchCity.output)
|
.output(locationSearchCityOutput)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
||||||
return (await res.json()) as z.infer<typeof validation.location.searchCity.output>;
|
return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import type { InferInsertModel } from "@homarr/db";
|
|||||||
import { and, createId, desc, eq, like } from "@homarr/db";
|
import { and, createId, desc, eq, like } from "@homarr/db";
|
||||||
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
||||||
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
||||||
import { validation } from "@homarr/validation";
|
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||||
|
import { mediaUploadSchema } from "@homarr/validation/media";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
||||||
|
|
||||||
export const mediaRouter = createTRPCRouter({
|
export const mediaRouter = createTRPCRouter({
|
||||||
getPaginated: protectedProcedure
|
getPaginated: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
validation.common.paginated.and(
|
paginatedSchema.and(
|
||||||
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
|
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -51,7 +52,7 @@ export const mediaRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
uploadMedia: permissionRequiredProcedure
|
uploadMedia: permissionRequiredProcedure
|
||||||
.requiresPermission("media-upload")
|
.requiresPermission("media-upload")
|
||||||
.input(validation.media.uploadMedia)
|
.input(mediaUploadSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const content = Buffer.from(await input.file.arrayBuffer());
|
const content = Buffer.from(await input.file.arrayBuffer());
|
||||||
const id = createId();
|
const id = createId();
|
||||||
@@ -82,7 +83,7 @@ export const mediaRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
deleteMedia: protectedProcedure.input(validation.common.byId).mutation(async ({ ctx, input }) => {
|
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||||
const dbMedia = await ctx.db.query.medias.findFirst({
|
const dbMedia = await ctx.db.query.medias.findFirst({
|
||||||
where: eq(medias.id, input.id),
|
where: eq(medias.id, input.id),
|
||||||
columns: {
|
columns: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { onboarding } from "@homarr/db/schema";
|
import { onboarding } from "@homarr/db/schema";
|
||||||
import { onboardingSteps } from "@homarr/definitions";
|
import { onboardingSteps } from "@homarr/definitions";
|
||||||
import { zodEnumFromArray } from "@homarr/validation";
|
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
|
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { asc, createId, eq, like } from "@homarr/db";
|
|||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { searchEngines, users } from "@homarr/db/schema";
|
import { searchEngines, users } from "@homarr/db/schema";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { validation } from "@homarr/validation";
|
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||||
|
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||||
|
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
export const searchEngineRouter = createTRPCRouter({
|
export const searchEngineRouter = createTRPCRouter({
|
||||||
getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => {
|
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
|
||||||
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
||||||
const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery);
|
const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery);
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
.then((engines) => engines.map((engine) => ({ value: engine.id, label: engine.name })));
|
.then((engines) => engines.map((engine) => ({ value: engine.id, label: engine.name })));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
byId: protectedProcedure.input(validation.common.byId).query(async ({ ctx, input }) => {
|
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
|
||||||
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
||||||
where: eq(searchEngines.id, input.id),
|
where: eq(searchEngines.id, input.id),
|
||||||
});
|
});
|
||||||
@@ -115,7 +117,7 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
|
search: protectedProcedure.input(searchSchema).query(async ({ ctx, input }) => {
|
||||||
return await ctx.db.query.searchEngines.findMany({
|
return await ctx.db.query.searchEngines.findMany({
|
||||||
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
|
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
|
||||||
with: {
|
with: {
|
||||||
@@ -132,21 +134,21 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
getMediaRequestOptions: protectedProcedure
|
getMediaRequestOptions: protectedProcedure
|
||||||
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||||
.input(validation.common.mediaRequestOptions)
|
.input(mediaRequestOptionsSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const integration = await createIntegrationAsync(ctx.integration);
|
const integration = await createIntegrationAsync(ctx.integration);
|
||||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||||
}),
|
}),
|
||||||
requestMedia: protectedProcedure
|
requestMedia: protectedProcedure
|
||||||
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||||
.input(validation.common.requestMedia)
|
.input(mediaRequestRequestSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const integration = await createIntegrationAsync(ctx.integration);
|
const integration = await createIntegrationAsync(ctx.integration);
|
||||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("search-engine-create")
|
.requiresPermission("search-engine-create")
|
||||||
.input(validation.searchEngine.manage)
|
.input(searchEngineManageSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db.insert(searchEngines).values({
|
await ctx.db.insert(searchEngines).values({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -161,7 +163,7 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
update: permissionRequiredProcedure
|
update: permissionRequiredProcedure
|
||||||
.requiresPermission("search-engine-modify-all")
|
.requiresPermission("search-engine-modify-all")
|
||||||
.input(validation.searchEngine.edit)
|
.input(searchEngineEditSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
||||||
where: eq(searchEngines.id, input.id),
|
where: eq(searchEngines.id, input.id),
|
||||||
@@ -188,7 +190,7 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
delete: permissionRequiredProcedure
|
delete: permissionRequiredProcedure
|
||||||
.requiresPermission("search-engine-full-all")
|
.requiresPermission("search-engine-full-all")
|
||||||
.input(validation.common.byId)
|
.input(byIdSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import type { ServerSettings } from "@homarr/server-settings";
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
import { validation } from "@homarr/validation";
|
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||||
|
|
||||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
@@ -32,7 +32,7 @@ export const serverSettingsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
initSettings: onboardingProcedure
|
initSettings: onboardingProcedure
|
||||||
.requiresStep("settings")
|
.requiresStep("settings")
|
||||||
.input(validation.settings.init)
|
.input(settingsInitSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics);
|
await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics);
|
||||||
await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing);
|
await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing);
|
||||||
|
|||||||
@@ -10,7 +10,20 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
|
|||||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { validation } from "@homarr/validation";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
|
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||||
|
import {
|
||||||
|
userChangeColorSchemeSchema,
|
||||||
|
userChangeHomeBoardsSchema,
|
||||||
|
userChangePasswordApiSchema,
|
||||||
|
userChangeSearchPreferencesSchema,
|
||||||
|
userCreateSchema,
|
||||||
|
userEditProfileSchema,
|
||||||
|
userFirstDayOfWeekSchema,
|
||||||
|
userInitSchema,
|
||||||
|
userPingIconsEnabledSchema,
|
||||||
|
userRegistrationApiSchema,
|
||||||
|
} from "@homarr/validation/user";
|
||||||
|
|
||||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +41,7 @@ import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from
|
|||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
initUser: onboardingProcedure
|
initUser: onboardingProcedure
|
||||||
.requiresStep("user")
|
.requiresStep("user")
|
||||||
.input(validation.user.init)
|
.input(userInitSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
|
|
||||||
@@ -52,7 +65,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||||
}),
|
}),
|
||||||
register: publicProcedure
|
register: publicProcedure
|
||||||
.input(validation.user.registrationApi)
|
.input(userRegistrationApiSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
@@ -82,7 +95,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
|
||||||
.input(validation.user.create)
|
.input(userCreateSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
@@ -259,7 +272,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
return user;
|
return user;
|
||||||
}),
|
}),
|
||||||
editProfile: protectedProcedure
|
editProfile: protectedProcedure
|
||||||
.input(validation.user.editProfile)
|
.input(userEditProfileSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -318,7 +331,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
await ctx.db.delete(users).where(eq(users.id, input.userId));
|
await ctx.db.delete(users).where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
changePassword: protectedProcedure
|
changePassword: protectedProcedure
|
||||||
.input(validation.user.changePasswordApi)
|
.input(userChangePasswordApiSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -384,7 +397,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(users.id, input.userId));
|
.where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
changeHomeBoards: protectedProcedure
|
changeHomeBoards: protectedProcedure
|
||||||
.input(convertIntersectionToZodObject(validation.user.changeHomeBoards.and(z.object({ userId: z.string() }))))
|
.input(convertIntersectionToZodObject(userChangeHomeBoardsSchema.and(z.object({ userId: z.string() }))))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -430,7 +443,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
changeDefaultSearchEngine: protectedProcedure
|
changeDefaultSearchEngine: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
convertIntersectionToZodObject(
|
convertIntersectionToZodObject(
|
||||||
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
|
userChangeSearchPreferencesSchema.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
@@ -457,7 +470,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
|
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
|
||||||
}),
|
}),
|
||||||
changeColorScheme: protectedProcedure
|
changeColorScheme: protectedProcedure
|
||||||
.input(validation.user.changeColorScheme)
|
.input(userChangeColorSchemeSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -469,7 +482,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(users.id, ctx.session.user.id));
|
.where(eq(users.id, ctx.session.user.id));
|
||||||
}),
|
}),
|
||||||
changePingIconsEnabled: protectedProcedure
|
changePingIconsEnabled: protectedProcedure
|
||||||
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
.input(userPingIconsEnabledSchema.and(byIdSchema))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
// Only admins can change other users ping icons enabled
|
// Only admins can change other users ping icons enabled
|
||||||
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
|
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
|
||||||
@@ -487,7 +500,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(users.id, ctx.session.user.id));
|
.where(eq(users.id, ctx.session.user.id));
|
||||||
}),
|
}),
|
||||||
changeFirstDayOfWeek: protectedProcedure
|
changeFirstDayOfWeek: protectedProcedure
|
||||||
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
.input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema)))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -522,7 +535,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validation.user.baseCreate>, "groupIds">) => {
|
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
|
||||||
const salt = await createSaltAsync();
|
const salt = await createSaltAsync();
|
||||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import type { Modify } from "@homarr/common/types";
|
|||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { validation } from "@homarr/validation";
|
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
|
export const changeSearchPreferencesInputSchema = userChangeSearchPreferencesSchema.and(
|
||||||
z.object({ userId: z.string() }),
|
z.object({ userId: z.string() }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { mediaRequestsRouter } from "./media-requests";
|
|||||||
import { mediaServerRouter } from "./media-server";
|
import { mediaServerRouter } from "./media-server";
|
||||||
import { mediaTranscodingRouter } from "./media-transcoding";
|
import { mediaTranscodingRouter } from "./media-transcoding";
|
||||||
import { minecraftRouter } from "./minecraft";
|
import { minecraftRouter } from "./minecraft";
|
||||||
|
import { networkControllerRouter } from "./network-controller";
|
||||||
import { notebookRouter } from "./notebook";
|
import { notebookRouter } from "./notebook";
|
||||||
import { optionsRouter } from "./options";
|
import { optionsRouter } from "./options";
|
||||||
import { rssFeedRouter } from "./rssFeed";
|
import { rssFeedRouter } from "./rssFeed";
|
||||||
@@ -33,4 +34,5 @@ export const widgetRouter = createTRPCRouter({
|
|||||||
mediaTranscoding: mediaTranscodingRouter,
|
mediaTranscoding: mediaTranscodingRouter,
|
||||||
minecraft: minecraftRouter,
|
minecraft: minecraftRouter,
|
||||||
options: optionsRouter,
|
options: optionsRouter,
|
||||||
|
networkController: networkControllerRouter,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
||||||
import { validation } from "@homarr/validation";
|
import { paginatedSchema } from "@homarr/validation/common";
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
@@ -12,7 +12,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
|
|||||||
export const mediaTranscodingRouter = createTRPCRouter({
|
export const mediaTranscodingRouter = createTRPCRouter({
|
||||||
getDataAsync: publicProcedure
|
getDataAsync: publicProcedure
|
||||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||||
.input(validation.common.paginated.pick({ page: true, pageSize: true }))
|
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||||
pageOffset: input.page,
|
pageOffset: input.page,
|
||||||
|
|||||||
62
packages/api/src/router/widgets/network-controller.ts
Normal file
62
packages/api/src/router/widgets/network-controller.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import type { Integration } from "@homarr/db/schema";
|
||||||
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import type { NetworkControllerSummary } from "@homarr/integrations/types";
|
||||||
|
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";
|
||||||
|
|
||||||
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const networkControllerRouter = createTRPCRouter({
|
||||||
|
summary: publicProcedure
|
||||||
|
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = networkControllerRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integration: {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
kind: integration.kind,
|
||||||
|
},
|
||||||
|
summary: data,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
|
||||||
|
subscribeToSummary: publicProcedure
|
||||||
|
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"networkController"> }>;
|
||||||
|
summary: NetworkControllerSummary;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const innerHandler = networkControllerRequestHandler.handler(integrationWithSecrets, {});
|
||||||
|
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -3,9 +3,10 @@ import SuperJSON from "superjson";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { boards, items } from "@homarr/db/schema";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||||
|
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
||||||
|
|
||||||
export const notebookRouter = createTRPCRouter({
|
export const notebookRouter = createTRPCRouter({
|
||||||
updateContent: protectedProcedure
|
updateContent: protectedProcedure
|
||||||
@@ -17,6 +18,8 @@ export const notebookRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
|
||||||
|
|
||||||
const item = await ctx.db.query.items.findFirst({
|
const item = await ctx.db.query.items.findFirst({
|
||||||
where: eq(items.id, input.itemId),
|
where: eq(items.id, input.itemId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fetchWithTimeout } from "@homarr/common";
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
import { validation } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const atLocationInput = z.object({
|
||||||
|
longitude: z.number(),
|
||||||
|
latitude: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const atLocationOutput = z.object({
|
||||||
|
current_weather: z.object({
|
||||||
|
weathercode: z.number(),
|
||||||
|
temperature: z.number(),
|
||||||
|
windspeed: z.number(),
|
||||||
|
}),
|
||||||
|
daily: z.object({
|
||||||
|
time: z.array(z.string()),
|
||||||
|
weathercode: z.array(z.number()),
|
||||||
|
temperature_2m_max: z.array(z.number()),
|
||||||
|
temperature_2m_min: z.array(z.number()),
|
||||||
|
sunrise: z.array(z.string()),
|
||||||
|
sunset: z.array(z.string()),
|
||||||
|
wind_speed_10m_max: z.array(z.number()),
|
||||||
|
wind_gusts_10m_max: z.array(z.number()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const weatherRouter = createTRPCRouter({
|
export const weatherRouter = createTRPCRouter({
|
||||||
atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => {
|
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||||
const res = await fetchWithTimeout(
|
const res = await fetchWithTimeout(
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`,
|
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`,
|
||||||
);
|
);
|
||||||
const json: unknown = await res.json();
|
const json: unknown = await res.json();
|
||||||
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
|
const weather = await atLocationOutput.parseAsync(json);
|
||||||
return {
|
return {
|
||||||
current: weather.current_weather,
|
current: weather.current_weather,
|
||||||
daily: weather.daily.time.map((value, index) => {
|
daily: weather.daily.time.map((value, index) => {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "7.3.3",
|
"ldapts": "7.4.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.5",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"pretty-print-error": "^1.1.2",
|
"pretty-print-error": "^1.1.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/cookies": "0.9.0",
|
"@types/cookies": "0.9.0",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { Database } from "@homarr/db";
|
|||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
export const authorizeWithBasicCredentialsAsync = async (
|
export const authorizeWithBasicCredentialsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
credentials: z.infer<typeof validation.user.signIn>,
|
credentials: z.infer<typeof userSignInSchema>,
|
||||||
) => {
|
) => {
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),
|
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
|
|||||||
import { and, createId, eq } from "@homarr/db";
|
import { and, createId, eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { env } from "../../../env";
|
import { env } from "../../../env";
|
||||||
import { LdapClient } from "../ldap-client";
|
import { LdapClient } from "../ldap-client";
|
||||||
|
|
||||||
export const authorizeWithLdapCredentialsAsync = async (
|
export const authorizeWithLdapCredentialsAsync = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
credentials: z.infer<typeof validation.user.signIn>,
|
credentials: z.infer<typeof userSignInSchema>,
|
||||||
) => {
|
) => {
|
||||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||||
const client = new LdapClient();
|
const client = new LdapClient();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Credentials from "@auth/core/providers/credentials";
|
import type Credentials from "@auth/core/providers/credentials";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { validation } from "@homarr/validation";
|
import { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||||
@@ -15,7 +15,7 @@ export const createCredentialsConfiguration = (db: Database) =>
|
|||||||
name: "Credentials",
|
name: "Credentials",
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const data = await validation.user.signIn.parseAsync(credentials);
|
const data = await userSignInSchema.parseAsync(credentials);
|
||||||
|
|
||||||
return await authorizeWithBasicCredentialsAsync(db, data);
|
return await authorizeWithBasicCredentialsAsync(db, data);
|
||||||
},
|
},
|
||||||
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
|
|||||||
name: "Ldap",
|
name: "Ldap",
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const data = await validation.user.signIn.parseAsync(credentials);
|
const data = await userSignInSchema.parseAsync(credentials);
|
||||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||||
},
|
},
|
||||||
}) satisfies CredentialsConfiguration;
|
}) satisfies CredentialsConfiguration;
|
||||||
|
|||||||
@@ -32,7 +32,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { generateSecureRandomToken } from "@homarr/common/server";
|
|||||||
import { and, count, createId, db, eq } from "@homarr/db";
|
import { and, count, createId, db, eq } from "@homarr/db";
|
||||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||||
import { usernameSchema } from "@homarr/validation";
|
import { usernameSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
export const recreateAdmin = command({
|
export const recreateAdmin = command({
|
||||||
name: "recreate-admin",
|
name: "recreate-admin",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/env": "workspace:^0.1.0",
|
"@homarr/env": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "15.2.4",
|
"next": "15.2.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"undici": "7.7.0",
|
"undici": "7.7.0",
|
||||||
@@ -40,7 +40,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { objectKeys } from "@homarr/common";
|
import { objectKeys } from "@homarr/common";
|
||||||
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
||||||
import { createSubPubChannel } from "@homarr/redis";
|
import { createSubPubChannel } from "@homarr/redis";
|
||||||
import { zodEnumFromArray } from "@homarr/validation";
|
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||||
|
|
||||||
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
|
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ export const cronJobs = {
|
|||||||
updateChecker: { preventManualExecution: false },
|
updateChecker: { preventManualExecution: false },
|
||||||
mediaTranscoding: { preventManualExecution: false },
|
mediaTranscoding: { preventManualExecution: false },
|
||||||
minecraftServerStatus: { preventManualExecution: false },
|
minecraftServerStatus: { preventManualExecution: false },
|
||||||
|
networkController: { preventManualExecution: false },
|
||||||
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
|
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@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-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { mediaOrganizerJob } from "./jobs/integrations/media-organizer";
|
|||||||
import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests";
|
import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests";
|
||||||
import { mediaServerJob } from "./jobs/integrations/media-server";
|
import { mediaServerJob } from "./jobs/integrations/media-server";
|
||||||
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
||||||
|
import { networkControllerJob } from "./jobs/integrations/network-controller";
|
||||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||||
import { pingJob } from "./jobs/ping";
|
import { pingJob } from "./jobs/ping";
|
||||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||||
@@ -34,6 +35,7 @@ export const jobGroup = createCronJobGroup({
|
|||||||
updateChecker: updateCheckerJob,
|
updateChecker: updateCheckerJob,
|
||||||
mediaTranscoding: mediaTranscodingJob,
|
mediaTranscoding: mediaTranscodingJob,
|
||||||
minecraftServerStatus: minecraftServerStatusJob,
|
minecraftServerStatus: minecraftServerStatusJob,
|
||||||
|
networkController: networkControllerJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||||
|
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||||
|
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";
|
||||||
|
|
||||||
|
import { createCronJob } from "../../lib";
|
||||||
|
|
||||||
|
export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, {
|
||||||
|
widgetKinds: ["networkControllerSummary"],
|
||||||
|
getInput: {
|
||||||
|
networkControllerSummary: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
"@homarr/env": "workspace:^0.1.0",
|
"@homarr/env": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.17.3",
|
"@mantine/core": "^7.17.4",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@testcontainers/mysql": "^10.24.0",
|
"@testcontainers/mysql": "^10.24.1",
|
||||||
"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",
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tsx": "4.19.3",
|
"tsx": "4.19.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -556,6 +556,185 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/1.12.0/widgets/stocks"
|
| "/docs/1.12.0/widgets/stocks"
|
||||||
| "/docs/1.12.0/widgets/video"
|
| "/docs/1.12.0/widgets/video"
|
||||||
| "/docs/1.12.0/widgets/weather"
|
| "/docs/1.12.0/widgets/weather"
|
||||||
|
| "/docs/1.13.1/tags"
|
||||||
|
| "/docs/1.13.1/tags/active-directory"
|
||||||
|
| "/docs/1.13.1/tags/ad-guard"
|
||||||
|
| "/docs/1.13.1/tags/ad-guard-home"
|
||||||
|
| "/docs/1.13.1/tags/administration"
|
||||||
|
| "/docs/1.13.1/tags/advanced"
|
||||||
|
| "/docs/1.13.1/tags/analytics"
|
||||||
|
| "/docs/1.13.1/tags/api"
|
||||||
|
| "/docs/1.13.1/tags/apps"
|
||||||
|
| "/docs/1.13.1/tags/banner"
|
||||||
|
| "/docs/1.13.1/tags/blocking"
|
||||||
|
| "/docs/1.13.1/tags/boards"
|
||||||
|
| "/docs/1.13.1/tags/bookmark"
|
||||||
|
| "/docs/1.13.1/tags/bookmarks"
|
||||||
|
| "/docs/1.13.1/tags/caddy"
|
||||||
|
| "/docs/1.13.1/tags/certificates"
|
||||||
|
| "/docs/1.13.1/tags/checklist"
|
||||||
|
| "/docs/1.13.1/tags/code"
|
||||||
|
| "/docs/1.13.1/tags/community"
|
||||||
|
| "/docs/1.13.1/tags/configuration"
|
||||||
|
| "/docs/1.13.1/tags/connections"
|
||||||
|
| "/docs/1.13.1/tags/customization"
|
||||||
|
| "/docs/1.13.1/tags/data-sources"
|
||||||
|
| "/docs/1.13.1/tags/database"
|
||||||
|
| "/docs/1.13.1/tags/developer"
|
||||||
|
| "/docs/1.13.1/tags/development"
|
||||||
|
| "/docs/1.13.1/tags/dns"
|
||||||
|
| "/docs/1.13.1/tags/docker"
|
||||||
|
| "/docs/1.13.1/tags/donation"
|
||||||
|
| "/docs/1.13.1/tags/edit-mode"
|
||||||
|
| "/docs/1.13.1/tags/env"
|
||||||
|
| "/docs/1.13.1/tags/environment-variables"
|
||||||
|
| "/docs/1.13.1/tags/feeds"
|
||||||
|
| "/docs/1.13.1/tags/finance"
|
||||||
|
| "/docs/1.13.1/tags/getting-started"
|
||||||
|
| "/docs/1.13.1/tags/google"
|
||||||
|
| "/docs/1.13.1/tags/grafana"
|
||||||
|
| "/docs/1.13.1/tags/groups"
|
||||||
|
| "/docs/1.13.1/tags/hardware"
|
||||||
|
| "/docs/1.13.1/tags/health"
|
||||||
|
| "/docs/1.13.1/tags/help"
|
||||||
|
| "/docs/1.13.1/tags/icon-picker"
|
||||||
|
| "/docs/1.13.1/tags/icon-repositories"
|
||||||
|
| "/docs/1.13.1/tags/icons"
|
||||||
|
| "/docs/1.13.1/tags/iframe"
|
||||||
|
| "/docs/1.13.1/tags/images"
|
||||||
|
| "/docs/1.13.1/tags/installation"
|
||||||
|
| "/docs/1.13.1/tags/integrade"
|
||||||
|
| "/docs/1.13.1/tags/integration"
|
||||||
|
| "/docs/1.13.1/tags/integrations"
|
||||||
|
| "/docs/1.13.1/tags/interface"
|
||||||
|
| "/docs/1.13.1/tags/jellyserr"
|
||||||
|
| "/docs/1.13.1/tags/layout"
|
||||||
|
| "/docs/1.13.1/tags/ldap"
|
||||||
|
| "/docs/1.13.1/tags/links"
|
||||||
|
| "/docs/1.13.1/tags/lists"
|
||||||
|
| "/docs/1.13.1/tags/management"
|
||||||
|
| "/docs/1.13.1/tags/market"
|
||||||
|
| "/docs/1.13.1/tags/media"
|
||||||
|
| "/docs/1.13.1/tags/minecraft"
|
||||||
|
| "/docs/1.13.1/tags/monitoring"
|
||||||
|
| "/docs/1.13.1/tags/news"
|
||||||
|
| "/docs/1.13.1/tags/notebook"
|
||||||
|
| "/docs/1.13.1/tags/notes"
|
||||||
|
| "/docs/1.13.1/tags/oidc"
|
||||||
|
| "/docs/1.13.1/tags/open-collective"
|
||||||
|
| "/docs/1.13.1/tags/open-media-vault"
|
||||||
|
| "/docs/1.13.1/tags/overseerr"
|
||||||
|
| "/docs/1.13.1/tags/permissions"
|
||||||
|
| "/docs/1.13.1/tags/pgid"
|
||||||
|
| "/docs/1.13.1/tags/pi-hole"
|
||||||
|
| "/docs/1.13.1/tags/ping"
|
||||||
|
| "/docs/1.13.1/tags/programming"
|
||||||
|
| "/docs/1.13.1/tags/proxmox"
|
||||||
|
| "/docs/1.13.1/tags/proxy"
|
||||||
|
| "/docs/1.13.1/tags/puid"
|
||||||
|
| "/docs/1.13.1/tags/responsive"
|
||||||
|
| "/docs/1.13.1/tags/roles"
|
||||||
|
| "/docs/1.13.1/tags/rss"
|
||||||
|
| "/docs/1.13.1/tags/search"
|
||||||
|
| "/docs/1.13.1/tags/search-engines"
|
||||||
|
| "/docs/1.13.1/tags/security"
|
||||||
|
| "/docs/1.13.1/tags/self-signed"
|
||||||
|
| "/docs/1.13.1/tags/seo"
|
||||||
|
| "/docs/1.13.1/tags/server"
|
||||||
|
| "/docs/1.13.1/tags/settings"
|
||||||
|
| "/docs/1.13.1/tags/sinkhole"
|
||||||
|
| "/docs/1.13.1/tags/sso"
|
||||||
|
| "/docs/1.13.1/tags/stocks"
|
||||||
|
| "/docs/1.13.1/tags/system"
|
||||||
|
| "/docs/1.13.1/tags/table"
|
||||||
|
| "/docs/1.13.1/tags/technical-documentation"
|
||||||
|
| "/docs/1.13.1/tags/text"
|
||||||
|
| "/docs/1.13.1/tags/torrent"
|
||||||
|
| "/docs/1.13.1/tags/traefik"
|
||||||
|
| "/docs/1.13.1/tags/translations"
|
||||||
|
| "/docs/1.13.1/tags/unraid"
|
||||||
|
| "/docs/1.13.1/tags/uploads"
|
||||||
|
| "/docs/1.13.1/tags/usenet"
|
||||||
|
| "/docs/1.13.1/tags/users"
|
||||||
|
| "/docs/1.13.1/tags/variables"
|
||||||
|
| "/docs/1.13.1/tags/widgets"
|
||||||
|
| "/docs/1.13.1/advanced/command-line"
|
||||||
|
| "/docs/1.13.1/advanced/command-line/fix-usernames"
|
||||||
|
| "/docs/1.13.1/advanced/command-line/password-recovery"
|
||||||
|
| "/docs/1.13.1/advanced/development/getting-started"
|
||||||
|
| "/docs/1.13.1/advanced/development/kubernetes"
|
||||||
|
| "/docs/1.13.1/advanced/environment-variables"
|
||||||
|
| "/docs/1.13.1/advanced/icons"
|
||||||
|
| "/docs/1.13.1/advanced/keyboard-shortcuts"
|
||||||
|
| "/docs/1.13.1/advanced/proxy"
|
||||||
|
| "/docs/1.13.1/advanced/running-as-different-user"
|
||||||
|
| "/docs/1.13.1/advanced/single-sign-on"
|
||||||
|
| "/docs/1.13.1/advanced/styling"
|
||||||
|
| "/docs/1.13.1/category/advanced"
|
||||||
|
| "/docs/1.13.1/category/community"
|
||||||
|
| "/docs/1.13.1/category/developer-guides"
|
||||||
|
| "/docs/1.13.1/category/getting-started"
|
||||||
|
| "/docs/1.13.1/category/installation"
|
||||||
|
| "/docs/1.13.1/category/installation-1"
|
||||||
|
| "/docs/1.13.1/category/integrations"
|
||||||
|
| "/docs/1.13.1/category/management"
|
||||||
|
| "/docs/1.13.1/category/widgets"
|
||||||
|
| "/docs/1.13.1/community/donate"
|
||||||
|
| "/docs/1.13.1/community/faq"
|
||||||
|
| "/docs/1.13.1/community/get-in-touch"
|
||||||
|
| "/docs/1.13.1/community/license"
|
||||||
|
| "/docs/1.13.1/community/translations"
|
||||||
|
| "/docs/1.13.1/getting-started"
|
||||||
|
| "/docs/1.13.1/getting-started/after-the-installation"
|
||||||
|
| "/docs/1.13.1/getting-started/glossary"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/docker"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/easy-panel"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/helm"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/home-assistant"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/portainer"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/proxmox"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/qnap"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/railway"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/saltbox"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/source"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/synology"
|
||||||
|
| "/docs/1.13.1/getting-started/installation/unraid"
|
||||||
|
| "/docs/1.13.1/integrations/cloud"
|
||||||
|
| "/docs/1.13.1/integrations/containers"
|
||||||
|
| "/docs/1.13.1/integrations/dns"
|
||||||
|
| "/docs/1.13.1/integrations/hardware"
|
||||||
|
| "/docs/1.13.1/integrations/kubernetes"
|
||||||
|
| "/docs/1.13.1/integrations/media-requester"
|
||||||
|
| "/docs/1.13.1/integrations/media-server"
|
||||||
|
| "/docs/1.13.1/integrations/servarr"
|
||||||
|
| "/docs/1.13.1/integrations/torrent"
|
||||||
|
| "/docs/1.13.1/integrations/usenet"
|
||||||
|
| "/docs/1.13.1/management/api"
|
||||||
|
| "/docs/1.13.1/management/apps"
|
||||||
|
| "/docs/1.13.1/management/boards"
|
||||||
|
| "/docs/1.13.1/management/certificates"
|
||||||
|
| "/docs/1.13.1/management/integrations"
|
||||||
|
| "/docs/1.13.1/management/media"
|
||||||
|
| "/docs/1.13.1/management/search-engines"
|
||||||
|
| "/docs/1.13.1/management/settings"
|
||||||
|
| "/docs/1.13.1/management/users"
|
||||||
|
| "/docs/1.13.1/widgets/bookmarks"
|
||||||
|
| "/docs/1.13.1/widgets/calendar"
|
||||||
|
| "/docs/1.13.1/widgets/clock"
|
||||||
|
| "/docs/1.13.1/widgets/dns-hole"
|
||||||
|
| "/docs/1.13.1/widgets/downloads"
|
||||||
|
| "/docs/1.13.1/widgets/health-monitoring"
|
||||||
|
| "/docs/1.13.1/widgets/home-assistant"
|
||||||
|
| "/docs/1.13.1/widgets/iframe"
|
||||||
|
| "/docs/1.13.1/widgets/indexer-manager"
|
||||||
|
| "/docs/1.13.1/widgets/media-requests"
|
||||||
|
| "/docs/1.13.1/widgets/media-server"
|
||||||
|
| "/docs/1.13.1/widgets/minecraft-server-status"
|
||||||
|
| "/docs/1.13.1/widgets/notebook"
|
||||||
|
| "/docs/1.13.1/widgets/rss"
|
||||||
|
| "/docs/1.13.1/widgets/stocks"
|
||||||
|
| "/docs/1.13.1/widgets/video"
|
||||||
|
| "/docs/1.13.1/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"
|
||||||
|
|||||||
@@ -157,6 +157,12 @@ export const integrationDefs = {
|
|||||||
category: ["calendar"],
|
category: ["calendar"],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
|
||||||
},
|
},
|
||||||
|
unifiController: {
|
||||||
|
name: "Unifi Controller",
|
||||||
|
secretKinds: [["username", "password"]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
||||||
|
category: ["networkController"],
|
||||||
|
},
|
||||||
} as const satisfies Record<string, integrationDefinition>;
|
} as const satisfies Record<string, integrationDefinition>;
|
||||||
|
|
||||||
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
||||||
@@ -209,4 +215,5 @@ export type IntegrationCategory =
|
|||||||
| "indexerManager"
|
| "indexerManager"
|
||||||
| "healthMonitoring"
|
| "healthMonitoring"
|
||||||
| "search"
|
| "search"
|
||||||
| "mediaTranscoding";
|
| "mediaTranscoding"
|
||||||
|
| "networkController";
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const widgetKinds = [
|
|||||||
"mediaRequests-requestStats",
|
"mediaRequests-requestStats",
|
||||||
"mediaTranscoding",
|
"mediaTranscoding",
|
||||||
"minecraftServerStatus",
|
"minecraftServerStatus",
|
||||||
|
"networkControllerSummary",
|
||||||
|
"networkControllerStatus",
|
||||||
"rssFeed",
|
"rssFeed",
|
||||||
"bookmarks",
|
"bookmarks",
|
||||||
"indexerManager",
|
"indexerManager",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@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.37",
|
"@types/dockerode": "^3.3.37",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/env/package.json
vendored
4
packages/env/package.json
vendored
@@ -30,7 +30,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,14 +26,14 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^7.17.3",
|
"@mantine/form": "^7.17.4",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
|
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
|
||||||
|
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { zodErrorMap } from "@homarr/validation/form";
|
import { zodErrorMap } from "@homarr/validation/form/i18n";
|
||||||
|
|
||||||
export const useZodForm = <
|
export const useZodForm = <
|
||||||
TSchema extends
|
TSchema extends
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.17.3",
|
"@mantine/core": "^7.17.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ interface IconPickerProps {
|
|||||||
error?: string | null;
|
error?: string | null;
|
||||||
onFocus?: FocusEventHandler;
|
onFocus?: FocusEventHandler;
|
||||||
onBlur?: FocusEventHandler;
|
onBlur?: FocusEventHandler;
|
||||||
|
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
withAsterisk?: boolean;
|
withAsterisk?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +46,8 @@ export const IconPicker = ({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
withAsterisk = true,
|
withAsterisk = true,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
}: IconPickerProps) => {
|
}: IconPickerProps) => {
|
||||||
const [value, setValue] = useUncontrolled({
|
const [value, setValue] = useUncontrolled({
|
||||||
value: propsValue,
|
value: propsValue,
|
||||||
@@ -155,8 +160,8 @@ export const IconPicker = ({
|
|||||||
rightSectionPointerEvents="none"
|
rightSectionPointerEvents="none"
|
||||||
withAsterisk={withAsterisk}
|
withAsterisk={withAsterisk}
|
||||||
error={error}
|
error={error}
|
||||||
label={tCommon("iconPicker.label")}
|
label={label ?? tCommon("iconPicker.label")}
|
||||||
placeholder={tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
|
placeholder={placeholder ?? tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
|
||||||
/>
|
/>
|
||||||
{session?.user.permissions.includes("media-upload") && (
|
{session?.user.permissions.includes("media-upload") && (
|
||||||
<UploadMedia
|
<UploadMedia
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
import { AppForm } from "./_form";
|
import { AppForm } from "./_form";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const AppNewForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => {
|
(values: z.infer<typeof appManageSchema>, redirect: boolean, afterSuccess?: () => void) => {
|
||||||
mutate(values, {
|
mutate(values, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import type { z } from "zod";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
import { IconPicker } from "../icon-picker/icon-picker";
|
import { IconPicker } from "../icon-picker/icon-picker";
|
||||||
import { findBestIconMatch } from "./icon-matcher";
|
import { findBestIconMatch } from "./icon-matcher";
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.app.manage>;
|
type FormType = z.infer<typeof appManageSchema>;
|
||||||
|
|
||||||
interface AppFormProps {
|
interface AppFormProps {
|
||||||
showBackToOverview: boolean;
|
showBackToOverview: boolean;
|
||||||
@@ -37,7 +37,7 @@ export const AppForm = ({
|
|||||||
}: AppFormProps) => {
|
}: AppFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const form = useZodForm(validation.app.manage, {
|
const form = useZodForm(appManageSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: initialValues?.name ?? "",
|
name: initialValues?.name ?? "",
|
||||||
description: initialValues?.description ?? "",
|
description: initialValues?.description ?? "",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { clientApi } from "@homarr/api/client";
|
|||||||
import type { MaybePromise } from "@homarr/common/types";
|
import type { MaybePromise } from "@homarr/common/types";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { supportedMediaUploadFormats } from "@homarr/validation";
|
import { supportedMediaUploadFormats } from "@homarr/validation/media";
|
||||||
|
|
||||||
interface UploadMediaProps {
|
interface UploadMediaProps {
|
||||||
children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;
|
children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;
|
||||||
|
|||||||
@@ -31,7 +31,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"@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/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-fac
|
|||||||
import { PlexIntegration } from "../plex/plex-integration";
|
import { PlexIntegration } from "../plex/plex-integration";
|
||||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||||
|
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||||
import type { Integration, IntegrationInput } from "./integration";
|
import type { Integration, IntegrationInput } from "./integration";
|
||||||
|
|
||||||
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
|
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
|
||||||
@@ -88,6 +89,7 @@ export const integrationCreators = {
|
|||||||
proxmox: ProxmoxIntegration,
|
proxmox: ProxmoxIntegration,
|
||||||
emby: EmbyIntegration,
|
emby: EmbyIntegration,
|
||||||
nextcloud: NextcloudIntegration,
|
nextcloud: NextcloudIntegration,
|
||||||
|
unifiController: UnifiControllerIntegration,
|
||||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||||
|
|
||||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export type TestConnectionResult =
|
|||||||
success: true;
|
success: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const throwErrorByStatusCode = (statusCode: number) => {
|
export const throwErrorByStatusCode = (statusCode: number) => {
|
||||||
switch (statusCode) {
|
switch (statusCode) {
|
||||||
case 400:
|
case 400:
|
||||||
throw new IntegrationTestConnectionError("badRequest");
|
throw new IntegrationTestConnectionError("badRequest");
|
||||||
@@ -124,6 +124,8 @@ const throwErrorByStatusCode = (statusCode: number) => {
|
|||||||
throw new IntegrationTestConnectionError("forbidden");
|
throw new IntegrationTestConnectionError("forbidden");
|
||||||
case 404:
|
case 404:
|
||||||
throw new IntegrationTestConnectionError("notFound");
|
throw new IntegrationTestConnectionError("notFound");
|
||||||
|
case 429:
|
||||||
|
throw new IntegrationTestConnectionError("tooManyRequests");
|
||||||
case 500:
|
case 500:
|
||||||
throw new IntegrationTestConnectionError("internalServerError");
|
throw new IntegrationTestConnectionError("internalServerError");
|
||||||
case 503:
|
case 503:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { NetworkControllerSummary } from "./network-controller-summary-types";
|
||||||
|
|
||||||
|
export interface NetworkControllerSummaryIntegration {
|
||||||
|
getNetworkSummaryAsync(): Promise<NetworkControllerSummary>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface NetworkControllerSummary {
|
||||||
|
wanStatus: "enabled" | "disabled";
|
||||||
|
|
||||||
|
www: {
|
||||||
|
status: "enabled" | "disabled";
|
||||||
|
latency: number;
|
||||||
|
ping: number;
|
||||||
|
uptime: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
wifi: {
|
||||||
|
status: "enabled" | "disabled";
|
||||||
|
users: number;
|
||||||
|
guests: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
lan: {
|
||||||
|
status: "enabled" | "disabled";
|
||||||
|
users: number;
|
||||||
|
guests: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
vpn: {
|
||||||
|
status: "enabled" | "disabled";
|
||||||
|
users: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
export * from "./calendar-types";
|
export * from "./calendar-types";
|
||||||
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||||
|
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
||||||
export * from "./interfaces/health-monitoring/healt-monitoring";
|
export * from "./interfaces/health-monitoring/healt-monitoring";
|
||||||
export * from "./interfaces/indexer-manager/indexer";
|
export * from "./interfaces/indexer-manager/indexer";
|
||||||
export * from "./interfaces/media-requests/media-request";
|
export * from "./interfaces/media-requests/media-request";
|
||||||
export * from "./base/searchable-integration";
|
export * from "./base/searchable-integration";
|
||||||
export * from "./homeassistant/homeassistant-types";
|
export * from "./homeassistant/homeassistant-types";
|
||||||
export * from "./proxmox/proxmox-types";
|
export * from "./proxmox/proxmox-types";
|
||||||
|
export * from "./unifi-controller/unifi-controller-types";
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { ParseError } from "../base/error";
|
||||||
|
import { Integration, throwErrorByStatusCode } from "../base/integration";
|
||||||
|
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||||
|
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
|
||||||
|
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
|
||||||
|
import { unifiSummaryResponseSchema } from "./unifi-controller-types";
|
||||||
|
|
||||||
|
const udmpPrefix = "proxy/network";
|
||||||
|
type Subsystem = "www" | "wan" | "wlan" | "lan" | "vpn";
|
||||||
|
|
||||||
|
export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration {
|
||||||
|
private prefix: string | undefined;
|
||||||
|
|
||||||
|
public async getNetworkSummaryAsync(): Promise<NetworkControllerSummary> {
|
||||||
|
if (!this.headers) {
|
||||||
|
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUrl = this.url(`/${this.prefix}/api/stat/sites`);
|
||||||
|
|
||||||
|
const requestHeaders: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...this.headers,
|
||||||
|
};
|
||||||
|
if (this.csrfToken) {
|
||||||
|
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsResponse = await fetchWithTrustedCertificatesAsync(requestUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
...requestHeaders,
|
||||||
|
},
|
||||||
|
}).catch((err: TypeError) => {
|
||||||
|
const detailMessage = String(err.cause);
|
||||||
|
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!statsResponse.ok) {
|
||||||
|
throwErrorByStatusCode(statsResponse.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = unifiSummaryResponseSchema.safeParse(await statsResponse.json());
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ParseError("Unifi controller", result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wanStatus: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
|
||||||
|
www: {
|
||||||
|
status: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
|
||||||
|
latency: this.getNumericValueOverAllSites(result.data, "www", (site) => site.latency, "max"),
|
||||||
|
ping: this.getNumericValueOverAllSites(result.data, "www", (site) => site.speedtest_ping, "max"),
|
||||||
|
uptime: this.getNumericValueOverAllSites(result.data, "www", (site) => site.uptime, "max"),
|
||||||
|
},
|
||||||
|
wifi: {
|
||||||
|
status: this.getStatusValueOverAllSites(result.data, "wlan", (site) => site.status === "ok"),
|
||||||
|
users: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_user, "sum"),
|
||||||
|
guests: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_guest, "sum"),
|
||||||
|
},
|
||||||
|
lan: {
|
||||||
|
status: this.getStatusValueOverAllSites(result.data, "lan", (site) => site.status === "ok"),
|
||||||
|
users: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_user, "sum"),
|
||||||
|
guests: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_guest, "sum"),
|
||||||
|
},
|
||||||
|
vpn: {
|
||||||
|
status: this.getStatusValueOverAllSites(result.data, "vpn", (site) => site.status === "ok"),
|
||||||
|
users: this.getNumericValueOverAllSites(result.data, "vpn", (site) => site.remote_user_num_active, "sum"),
|
||||||
|
},
|
||||||
|
} satisfies NetworkControllerSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testConnectionAsync(): Promise<void> {
|
||||||
|
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusValueOverAllSites(
|
||||||
|
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||||
|
subsystem: Subsystem,
|
||||||
|
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
|
||||||
|
) {
|
||||||
|
return this.getBooleanValueOverAllSites(data, subsystem, selectCallback) ? "enabled" : "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNumericValueOverAllSites<
|
||||||
|
S extends Subsystem,
|
||||||
|
T extends Extract<z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number], { subsystem: S }>,
|
||||||
|
>(
|
||||||
|
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||||
|
subsystem: S,
|
||||||
|
selectCallback: (obj: T) => number,
|
||||||
|
strategy: "average" | "sum" | "max",
|
||||||
|
): number {
|
||||||
|
const values = data.data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
|
||||||
|
|
||||||
|
if (strategy === "sum") {
|
||||||
|
return values.reduce((first, second) => first + second, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategy === "average") {
|
||||||
|
return values.reduce((first, second, _, array) => first + second / array.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(...values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBooleanValueOverAllSites(
|
||||||
|
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||||
|
subsystem: Subsystem,
|
||||||
|
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
|
||||||
|
): boolean {
|
||||||
|
return data.data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSubsystem(
|
||||||
|
health: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"],
|
||||||
|
subsystem: Subsystem,
|
||||||
|
) {
|
||||||
|
const value = health.find((health) => health.subsystem === subsystem);
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Subsystem ${subsystem} not found!`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers: Record<string, string> | undefined = undefined;
|
||||||
|
private csrfToken: string | undefined;
|
||||||
|
|
||||||
|
private async authenticateAndConstructSessionInHeaderAsync(): Promise<void> {
|
||||||
|
await this.determineUDMVariantAsync();
|
||||||
|
await this.authenticateAndSetCookieAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authenticateAndSetCookieAsync(): Promise<void> {
|
||||||
|
if (this.headers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = this.prefix === udmpPrefix ? "auth/login" : "login";
|
||||||
|
logger.debug("Authenticating at network console: " + endpoint);
|
||||||
|
|
||||||
|
const loginUrl = this.url(`/api/${endpoint}`);
|
||||||
|
|
||||||
|
const loginBody = {
|
||||||
|
username: this.getSecretValue("username"),
|
||||||
|
password: this.getSecretValue("password"),
|
||||||
|
remember: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestHeaders: Record<string, string> = { "Content-Type": "application/json" };
|
||||||
|
if (this.csrfToken) {
|
||||||
|
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginResponse = await fetchWithTrustedCertificatesAsync(loginUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...requestHeaders,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(loginBody),
|
||||||
|
}).catch((err: TypeError) => {
|
||||||
|
const detailMessage = String(err.cause);
|
||||||
|
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!loginResponse.ok) {
|
||||||
|
throwErrorByStatusCode(loginResponse.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseHeaders = loginResponse.headers;
|
||||||
|
const newHeaders: Record<string, string> = {};
|
||||||
|
const loginToken = UnifiControllerIntegration.extractLoginTokenFromCookies(responseHeaders);
|
||||||
|
newHeaders.Cookie = `${loginToken};`;
|
||||||
|
this.headers = newHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async determineUDMVariantAsync(): Promise<void> {
|
||||||
|
if (this.prefix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Prefix for authentication not set; initial connect to determine UDM variant");
|
||||||
|
const url = this.url("/");
|
||||||
|
|
||||||
|
const { status, ok, headers } = await fetchWithTrustedCertificatesAsync(url, { method: "HEAD" })
|
||||||
|
.then((res) => res)
|
||||||
|
.catch((err: TypeError) => {
|
||||||
|
const detailMessage = String(err.cause);
|
||||||
|
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
throw new IntegrationTestConnectionError("invalidUrl", "status code: " + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = "";
|
||||||
|
if (headers.get("x-csrf-token") !== null) {
|
||||||
|
// Unifi OS < 3.2.5 passes & requires csrf-token
|
||||||
|
prefix = udmpPrefix;
|
||||||
|
const headersCSRFToken = headers.get("x-csrf-token");
|
||||||
|
if (headersCSRFToken) {
|
||||||
|
this.csrfToken = headersCSRFToken;
|
||||||
|
}
|
||||||
|
} else if (headers.get("access-control-expose-headers") !== null) {
|
||||||
|
// Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint
|
||||||
|
prefix = udmpPrefix;
|
||||||
|
}
|
||||||
|
this.prefix = prefix;
|
||||||
|
logger.debug("Final prefix: " + this.prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractLoginTokenFromCookies(headers: Headers): string {
|
||||||
|
const cookies = headers.get("set-cookie") ?? "";
|
||||||
|
const loginToken = cookies.split(";").find((cookie) => cookie.includes("TOKEN"));
|
||||||
|
|
||||||
|
if (loginToken) {
|
||||||
|
return loginToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Login token not found in cookies");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const healthSchema = z.discriminatedUnion("subsystem", [
|
||||||
|
z.object({
|
||||||
|
subsystem: z.literal("wlan"),
|
||||||
|
num_user: z.number(),
|
||||||
|
num_guest: z.number(),
|
||||||
|
num_iot: z.number(),
|
||||||
|
"tx_bytes-r": z.number(),
|
||||||
|
"rx_bytes-r": z.number(),
|
||||||
|
status: z.string(),
|
||||||
|
num_ap: z.number(),
|
||||||
|
num_adopted: z.number(),
|
||||||
|
num_disabled: z.number(),
|
||||||
|
num_disconnected: z.number(),
|
||||||
|
num_pending: z.number(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subsystem: z.literal("wan"),
|
||||||
|
num_gw: z.number(),
|
||||||
|
num_adopted: z.number(),
|
||||||
|
num_disconnected: z.number(),
|
||||||
|
num_pending: z.number(),
|
||||||
|
status: z.string(),
|
||||||
|
wan_ip: z.string().ip(),
|
||||||
|
gateways: z.array(z.string().ip()),
|
||||||
|
netmask: z.string().ip(),
|
||||||
|
nameservers: z.array(z.string().ip()).optional(),
|
||||||
|
num_sta: z.number(),
|
||||||
|
"tx_bytes-r": z.number(),
|
||||||
|
"rx_bytes-r": z.number(),
|
||||||
|
gw_mac: z.string(),
|
||||||
|
gw_name: z.string(),
|
||||||
|
"gw_system-stats": z.object({
|
||||||
|
cpu: z.string(),
|
||||||
|
mem: z.string(),
|
||||||
|
uptime: z.string(),
|
||||||
|
}),
|
||||||
|
gw_version: z.string(),
|
||||||
|
isp_name: z.string(),
|
||||||
|
isp_organization: z.string(),
|
||||||
|
uptime_stats: z.object({
|
||||||
|
WAN: z.object({
|
||||||
|
alerting_monitors: z.array(
|
||||||
|
z.object({
|
||||||
|
availability: z.number(),
|
||||||
|
latency_average: z.number(),
|
||||||
|
target: z.string(),
|
||||||
|
type: z.enum(["icmp", "dns"]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
availability: z.number(),
|
||||||
|
latency_average: z.number(),
|
||||||
|
monitors: z.array(
|
||||||
|
z.object({
|
||||||
|
availability: z.number(),
|
||||||
|
latency_average: z.number(),
|
||||||
|
target: z.string(),
|
||||||
|
type: z.enum(["icmp", "dns"]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
time_period: z.number(),
|
||||||
|
uptime: z.number(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subsystem: z.literal("www"),
|
||||||
|
status: z.string(),
|
||||||
|
"tx_bytes-r": z.number(),
|
||||||
|
"rx_bytes-r": z.number(),
|
||||||
|
latency: z.number(),
|
||||||
|
uptime: z.number(),
|
||||||
|
drops: z.number(),
|
||||||
|
xput_up: z.number(),
|
||||||
|
xput_down: z.number(),
|
||||||
|
speedtest_status: z.string(),
|
||||||
|
speedtest_lastrun: z.number(),
|
||||||
|
speedtest_ping: z.number(),
|
||||||
|
gw_mac: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subsystem: z.literal("lan"),
|
||||||
|
lan_ip: z.string().ip().nullish(),
|
||||||
|
status: z.string(),
|
||||||
|
num_user: z.number(),
|
||||||
|
num_guest: z.number(),
|
||||||
|
num_iot: z.number(),
|
||||||
|
"tx_bytes-r": z.number(),
|
||||||
|
"rx_bytes-r": z.number(),
|
||||||
|
num_sw: z.number(),
|
||||||
|
num_adopted: z.number(),
|
||||||
|
num_disconnected: z.number(),
|
||||||
|
num_pending: z.number(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subsystem: z.literal("vpn"),
|
||||||
|
status: z.string(),
|
||||||
|
remote_user_enabled: z.boolean(),
|
||||||
|
remote_user_num_active: z.number(),
|
||||||
|
remote_user_num_inactive: z.number(),
|
||||||
|
remote_user_rx_bytes: z.number(),
|
||||||
|
remote_user_tx_bytes: z.number(),
|
||||||
|
remote_user_rx_packets: z.number(),
|
||||||
|
remote_user_tx_packets: z.number(),
|
||||||
|
site_to_site_enabled: z.boolean(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type Health = z.infer<typeof healthSchema>;
|
||||||
|
|
||||||
|
export const siteSchema = z.object({
|
||||||
|
anonymous_id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
external_id: z.string().uuid(),
|
||||||
|
_id: z.string(),
|
||||||
|
attr_no_delete: z.boolean(),
|
||||||
|
attr_hidden_id: z.string(),
|
||||||
|
desc: z.string(),
|
||||||
|
health: z.array(healthSchema),
|
||||||
|
num_new_alarms: z.number(),
|
||||||
|
});
|
||||||
|
export type Site = z.infer<typeof siteSchema>;
|
||||||
|
|
||||||
|
export const unifiSummaryResponseSchema = z.object({
|
||||||
|
meta: z.object({
|
||||||
|
rc: z.enum(["ok"]),
|
||||||
|
}),
|
||||||
|
data: z.array(siteSchema),
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.17.3",
|
"@mantine/core": "^7.17.4",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "15.2.4",
|
"next": "15.2.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
@@ -45,7 +45,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",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AppForm } from "@homarr/forms-collection";
|
|||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { validation } from "@homarr/validation";
|
import type { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
interface QuickAddAppModalProps {
|
interface QuickAddAppModalProps {
|
||||||
onClose: (createdAppId: string) => Promise<void>;
|
onClose: (createdAppId: string) => Promise<void>;
|
||||||
@@ -24,7 +24,7 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof validation.app.manage>) => {
|
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
|
||||||
mutate(values, {
|
mutate(values, {
|
||||||
async onSuccess({ appId }) {
|
async onSuccess({ appId }) {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user