Compare commits

...
Author SHA1 Message Date
Manuel 0db0c656b2 Merge branch 'dev' 2026-03-27 18:43:45 +01:00
homarr-renovate[bot] 22bbef9d8e fix(deps): update dependency undici to v7.24.5 (#5338)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-03-27 18:22:06 +01:00
Manuel 4f2b8217f8 fix: warnings in code 2026-03-27 18:12:27 +01:00
Kelian Baert 2fefd9da01 feat: add "allowModals" option for iframe widget (#5339) 2026-03-27 18:05:15 +01:00
Manuel b3e9b3351b Merge commit from fork 2026-03-27 17:50:26 +01:00
Lorinc936 d48254fa57 Merge branch 'dev' into main 2026-03-24 20:41:22 +01:00
Lorinc936 b2fdab0418 defined notificationSchema outside of class 2026-03-24 18:11:10 +01:00
Lorinc936 e78dc86aa3 Update nextcloud.integration.ts 2026-03-24 14:44:24 +01:00
Lorinc936andManuel e70d804ab8 Update packages/integrations/src/nextcloud/nextcloud.integration.ts
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2026-03-23 19:12:42 +01:00
Lorinc936 b7c3a48377 feat: add Nextcloud notification provider
fix: resolve hydration error in add-board modal
2026-03-23 17:25:22 +01:00
24 changed files with 105 additions and 57 deletions

No files matched your search

@@ -13,6 +13,7 @@ import type { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
import { userSignInSchema } from "@homarr/validation/user";
type Provider = "credentials" | "ldap" | "oidc";
@@ -70,7 +71,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
await revalidatePathActionAsync("/");
router.push(callbackUrl);
router.push(sanitizeRedirectionUrl(callbackUrl));
},
[t, router, callbackUrl],
);
@@ -4,6 +4,7 @@ import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { LoginForm } from "./_login-form";
@@ -19,7 +20,7 @@ export default async function Login(props: LoginProps) {
const session = await auth();
if (session) {
redirect(searchParams.callbackUrl ?? "/");
redirect(sanitizeRedirectionUrl(searchParams.callbackUrl));
}
const t = await getScopedI18n("user.page.login");
@@ -264,6 +264,5 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => {
window.removeEventListener("popstate", handlePopState);
window.removeEventListener("beforeunload", handleBeforeUnload);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDirty]);
};
@@ -27,7 +27,6 @@ export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
useEffect(() => {
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [board.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
@@ -14,7 +14,7 @@ import { ApiKeysManagement } from "./components/api-keys";
export async function generateMetadata() {
const session = await auth();
if (!session?.user || !session.user.permissions.includes("admin")) {
if (!session?.user.permissions.includes("admin")) {
return {};
}
@@ -27,7 +27,7 @@ export async function generateMetadata() {
export default async function ApiPage() {
const session = await auth();
if (!session?.user || !session.user.permissions.includes("admin")) {
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
@@ -18,7 +18,7 @@ import { LogContextProvider } from "./log-context";
export async function generateMetadata() {
const session = await auth();
if (!session?.user || !session.user.permissions.includes("admin")) {
if (!session?.user.permissions.includes("admin")) {
return {};
}
const t = await getScopedI18n("management");
@@ -30,7 +30,7 @@ export async function generateMetadata() {
export default async function LogsManagementPage() {
const session = await auth();
if (!session?.user || !session.user.permissions.includes("other-view-logs")) {
if (!session?.user.permissions.includes("other-view-logs")) {
notFound();
}
@@ -75,7 +75,6 @@ const executeHealthCheckSafelyAsync = async (
},
};
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error }));
return {
status: "unhealthy",
@@ -117,7 +117,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
when={
widgetSupportsIntegrations &&
item.integrationIds.length === 0 &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
}
/>
@@ -206,7 +206,6 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
}
// Only run this effect when the section items change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemIds.length, columnCount]);
/**
+1 -1
View File
@@ -41,7 +41,7 @@
"dotenv": "^17.3.1",
"fastify": "^5.8.2",
"superjson": "2.2.6",
"undici": "7.18.2"
"undici": "7.24.5"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"octokit": "^5.0.5",
"react": "19.2.4",
"react-dom": "19.2.4",
"undici": "7.18.2",
"undici": "7.24.5",
"zod": "^4.2.1",
"zod-validation-error": "^5.0.0"
},
+1 -1
View File
@@ -217,7 +217,7 @@ export const integrationDefs = {
name: "Nextcloud",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
category: ["calendar"],
category: ["calendar", "notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/nextcloud"),
},
unifiController: {
+1 -1
View File
@@ -47,7 +47,7 @@
"octokit": "^5.0.5",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.8",
"undici": "7.18.2",
"undici": "7.24.5",
"xml2js": "^0.6.2",
"zod": "^4.2.1"
},
@@ -5,8 +5,10 @@ import utc from "dayjs/plugin/utc";
import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
import * as ical from "node-ical";
import { DAVClient } from "tsdav";
import { z } from "zod";
import { createHttpsAgentAsync } from "@homarr/core/infrastructure/http";
import { ResponseError } from "@homarr/common/server";
import { createHttpsAgentAsync, fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { HandleIntegrationErrors } from "../base/errors/decorator";
@@ -16,6 +18,21 @@ import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
import type { Notification } from "../interfaces/notifications/notification-types";
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
const notificationSchema = z.object({
ocs: z.object({
data: z.array(
z.object({
notification_id: z.number(),
datetime: z.string(),
subject: z.string(),
message: z.string(),
}),
),
}),
});
const logger = createLogger({ module: "nextcloudIntegration" });
@@ -23,7 +40,7 @@ dayjs.extend(utc);
dayjs.extend(timezone);
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration implements ICalendarIntegration {
export class NextcloudIntegration extends Integration implements ICalendarIntegration, INotificationsIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options));
await client.login();
@@ -104,6 +121,29 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
.flat();
}
public async getNotificationsAsync(): Promise<Notification[]> {
const url = this.url("/ocs/v2.php/apps/notifications/api/v2/notifications", { format: "json" });
const response = await fetchWithTrustedCertificatesAsync(url, {
headers: {
Authorization: `Basic ${Buffer.from(
`${this.getSecretValue("username")}:${this.getSecretValue("password")}`,
).toString("base64")}`,
"OCS-APIRequest": "true",
},
});
if (!response.ok) throw new ResponseError(response);
const json = notificationSchema.parse(await response.json());
return json.ocs.data.map((notification) => ({
id: String(notification.notification_id),
time: new Date(notification.datetime),
title: notification.subject,
body: notification.message,
}));
}
private async createCalendarClientAsync(agent?: Agent) {
return new DAVClient({
serverUrl: this.integration.url,
@@ -57,10 +57,12 @@ export const AddBoardModal = createModal(({ actions }) => {
{...form.getInputProps("name")}
description={
boardNameStatus.description ? (
<Group c={boardNameStatus.description.color} gap="xs" align="center">
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
<span>{boardNameStatus.description.label}</span>
</Group>
<span>
<Group c={boardNameStatus.description.color} gap="xs" align="center">
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
<span>{boardNameStatus.description.label}</span>
</Group>
</span>
) : null
}
/>
@@ -101,6 +101,7 @@ const optionMapping: OptionMapping = {
allowMicrophone: (oldOptions) => oldOptions.allowMicrophone,
allowGeolocation: (oldOptions) => oldOptions.allowGeolocation,
allowScrolling: (oldOptions) => oldOptions.allowScrolling,
allowModals: () => undefined,
},
video: {
feedUrl: (oldOptions) => oldOptions.FeedUrl,
+1 -1
View File
@@ -33,7 +33,7 @@
"dayjs": "^1.11.20",
"octokit": "^5.0.5",
"superjson": "2.2.6",
"undici": "7.18.2",
"undici": "7.24.5",
"zod": "^4.2.1"
},
"devDependencies": {
@@ -94,7 +94,6 @@ const createSpotlightContext = (displayName: string) => {
context.unregisterItems(key);
};
// We ignore the results
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...dependencyArray, key]);
};
+3 -1
View File
@@ -1625,6 +1625,9 @@
},
"allowGeolocation": {
"label": "Allow geolocation"
},
"allowModals": {
"label": "Allow modals"
}
},
"error": {
@@ -2033,7 +2036,6 @@
"directStream": "Direct Stream"
}
},
"recentActivity": {
"title": "Recent Activity",
"empty": "No recent activity",
@@ -0,0 +1,14 @@
/**
* Sanitizes the URL from malicious payloads.
* Prevents open redirects such as //example.com/path
* and prevents XSS such as javascript:().
* Returns / as a default or when unsafe.
* @param url The URL to sanitize
*/
export const sanitizeRedirectionUrl = (url?: string) => {
if (url && url.startsWith("/") && !url.startsWith("//")) {
return url;
} else {
return "/"; // Safe default
}
};
@@ -105,6 +105,10 @@ const getSandboxFlags = (
baseSandbox.push("allow-popups-to-escape-sandbox");
}
if (permissions.allowModals) {
baseSandbox.push("allow-modals");
}
return baseSandbox;
};
@@ -115,4 +119,5 @@ const permissionMapping = {
allowGeolocation: "geolocation",
allowMicrophone: "microphone",
allowPayment: "payment",
allowModals: "allow-modals",
} satisfies Record<keyof Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">, string>;
+1
View File
@@ -17,6 +17,7 @@ export const { definition, componentLoader } = createWidgetDefinition("iframe",
allowMicrophone: factory.switch(),
allowCamera: factory.switch(),
allowGeolocation: factory.switch(),
allowModals: factory.switch(),
}));
},
}).withDynamicImport(() => import("./component"));
-1
View File
@@ -79,7 +79,6 @@ const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => {
],
});
// All other properties are updated with other useEffect
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoRef]);
useEffect(() => {
+20 -32
View File
@@ -438,8 +438,8 @@ importers:
specifier: 2.2.6
version: 2.2.6
undici:
specifier: 7.18.2
version: 7.18.2
specifier: 7.24.5
version: 7.24.5
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
@@ -862,8 +862,8 @@ importers:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
undici:
specifier: 7.18.2
version: 7.18.2
specifier: 7.24.5
version: 7.24.5
zod:
specifier: ^4.2.1
version: 4.2.1
@@ -1455,7 +1455,7 @@ importers:
version: link:../image-proxy
'@homarr/node-unifi':
specifier: ^2.6.0
version: 2.6.0(undici@7.18.2)
version: 2.6.0(undici@7.24.5)
'@homarr/redis':
specifier: workspace:^0.1.0
version: link:../redis
@@ -1493,8 +1493,8 @@ importers:
specifier: ^2.1.8
version: 2.1.8
undici:
specifier: 7.18.2
version: 7.18.2
specifier: 7.24.5
version: 7.24.5
xml2js:
specifier: ^0.6.2
version: 0.6.2
@@ -1864,8 +1864,8 @@ importers:
specifier: 2.2.6
version: 2.2.6
undici:
specifier: 7.18.2
version: 7.18.2
specifier: 7.24.5
version: 7.24.5
zod:
specifier: ^4.2.1
version: 4.2.1
@@ -10424,20 +10424,12 @@ packages:
resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==}
engines: {node: '>=18.17'}
undici@7.18.2:
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
engines: {node: '>=20.18.1'}
undici@7.21.0:
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
engines: {node: '>=20.18.1'}
undici@7.24.0:
resolution: {integrity: sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==}
engines: {node: '>=20.18.1'}
undici@7.24.4:
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
undici@7.24.5:
resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
engines: {node: '>=20.18.1'}
unicode-emoji-modifier-base@1.0.0:
@@ -11904,11 +11896,11 @@ snapshots:
'@homarr/gridstack@1.12.0': {}
'@homarr/node-unifi@2.6.0(undici@7.18.2)':
'@homarr/node-unifi@2.6.0(undici@7.24.5)':
dependencies:
axios: 1.9.0
eventemitter2: 6.4.9
http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.18.2)
http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.24.5)
tough-cookie: 5.1.2
ws: 8.19.0
transitivePeerDependencies:
@@ -12884,7 +12876,7 @@ snapshots:
p-filter: 4.1.0
semantic-release: 25.0.3(typescript@5.9.3)
tinyglobby: 0.2.15
undici: 7.18.2
undici: 7.24.5
url-join: 5.0.0
transitivePeerDependencies:
- supports-color
@@ -16593,12 +16585,12 @@ snapshots:
html-url-attributes@3.0.1: {}
http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.18.2):
http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.24.5):
dependencies:
agent-base: 7.1.3
tough-cookie: 5.1.2
optionalDependencies:
undici: 7.18.2
undici: 7.24.5
http-errors@2.0.0:
dependencies:
@@ -17080,7 +17072,7 @@ snapshots:
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.0
undici: 7.21.0
undici: 7.24.5
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
@@ -17107,7 +17099,7 @@ snapshots:
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.1
undici: 7.24.4
undici: 7.24.5
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
@@ -19740,7 +19732,7 @@ snapshots:
ssh-remote-port-forward: 1.0.4
tar-fs: 3.1.2
tmp: 0.2.5
undici: 7.24.4
undici: 7.24.5
transitivePeerDependencies:
- bare-buffer
- supports-color
@@ -20080,13 +20072,9 @@ snapshots:
undici@6.23.0: {}
undici@7.18.2: {}
undici@7.21.0: {}
undici@7.24.0: {}
undici@7.24.4: {}
undici@7.24.5: {}
unicode-emoji-modifier-base@1.0.0: {}