Compare commits

..
Author SHA1 Message Date
arturict 1ed6b16217 Keep existing Nextcloud DAV endpoints intact 2026-09-06 11:05:57 +02:00
Artur c6d22b3939 fix(integrations): preserve Nextcloud subpaths 2026-09-06 11:05:57 +02:00
8 changed files with 88 additions and 27 deletions

No files matched your search

+2 -3
View File
@@ -30,9 +30,8 @@ needed to restore encrypted integration credentials.
Select a Homarr backup ZIP. The browser previews entity counts, board names, and required database migrations before
the archive is uploaded.
Restoring replaces the current SQLite database and cannot be undone. It also invalidates the current database session.
After a successful management restore, Homarr waits for the restarted server and sends the administrator to sign in
again. The first-run onboarding restore keeps its existing board destination.
Restoring replaces the current SQLite database and cannot be undone. After a successful restore, the Next.js process
restarts and the page reloads when Homarr is ready.
A backup can be restored on another Homarr instance. When its `SECRET_ENCRYPTION_KEY` differs, Homarr re-encrypts stored
integration secrets for the target instance. Version metadata is used to apply compatible migrations.
@@ -204,12 +204,8 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
}
onRestoreComplete?.();
let destination: string | null = null;
if (variant === "standalone" && homeBoardName) {
destination = `/boards/${encodeURIComponent(homeBoardName)}`;
} else if (variant === "card") {
destination = "/auth/login";
}
const destination =
variant === "standalone" && homeBoardName ? `/boards/${encodeURIComponent(homeBoardName)}` : null;
startReadinessCheck(restartAfterMs, destination);
}, [file, onRestoreComplete, startReadinessCheck, t, variant]);
@@ -321,7 +317,6 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
<RestoreProgressPanel
active
status={restoreStatus}
reloginRequired={variant === "card"}
onRetry={() => startReadinessCheck(0)}
onReload={() => window.location.reload()}
/>
@@ -11,18 +11,11 @@ export type RestoreProgressStatus = "restoring" | "restarting" | "timedOut";
interface RestoreProgressPanelProps {
active: boolean;
status: RestoreProgressStatus;
reloginRequired?: boolean;
onRetry: () => void;
onReload: () => void;
}
export const RestoreProgressPanel = ({
active,
status,
reloginRequired = false,
onRetry,
onReload,
}: RestoreProgressPanelProps) => {
export const RestoreProgressPanel = ({ active, status, onRetry, onReload }: RestoreProgressPanelProps) => {
const t = useI18n("management.page.tool.backup.restore");
const tCommon = useI18n("common");
const reduceMotion = useReducedMotion();
@@ -70,12 +63,6 @@ export const RestoreProgressPanel = ({
</div>
</Group>
{reloginRequired && !isRestoring ? (
<Text size="sm" c="dimmed">
{t("progress.relogin")}
</Text>
) : null}
{isTimedOut ? (
<Group>
<Button variant="light" onClick={onRetry} leftSection={<IconRefresh size={16} />}>
@@ -0,0 +1,13 @@
export const createNextcloudCalendarServerUrl = (integrationUrl: string) => {
const calendarServerUrl = new URL(integrationUrl);
const integrationPath = calendarServerUrl.pathname.replace(/\/+$/, "");
const davPath = "/remote.php/dav";
calendarServerUrl.pathname = integrationPath.endsWith(davPath)
? `${integrationPath}/`
: `${integrationPath}${davPath}/`;
calendarServerUrl.search = "";
calendarServerUrl.hash = "";
return calendarServerUrl.toString();
};
@@ -21,6 +21,7 @@ import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integ
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
import type { Notification } from "../interfaces/notifications/notification-types";
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
import { createNextcloudCalendarServerUrl } from "./nextcloud-url";
// Register all existing timezones
if (ICAL.TimezoneService.count === 0) {
@@ -189,7 +190,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
private async createCalendarClientAsync(agent?: Agent) {
return new DAVClient({
serverUrl: this.integration.url,
serverUrl: createNextcloudCalendarServerUrl(this.integration.url),
credentials: {
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
@@ -0,0 +1,38 @@
import type { Agent } from "node:https";
import { DAVClient } from "tsdav";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { NextcloudIntegration } from "../nextcloud.integration";
vi.mock("tsdav", () => ({ DAVClient: vi.fn() }));
interface NextcloudCalendarClientFactory {
createCalendarClientAsync(agent: Agent): Promise<unknown>;
}
describe("NextcloudIntegration", () => {
beforeEach(() => {
vi.mocked(DAVClient).mockClear();
});
test("uses a calendar server URL that preserves the integration subpath", async () => {
const integration = new NextcloudIntegration({
id: "integration-id",
name: "Nextcloud",
url: "https://example.com/nextcloud",
externalUrl: null,
decryptedSecrets: [
{ kind: "username", value: "admin" },
{ kind: "password", value: "password" },
],
});
await (integration as unknown as NextcloudCalendarClientFactory).createCalendarClientAsync({} as Agent);
expect(DAVClient).toHaveBeenCalledWith(
expect.objectContaining({
serverUrl: "https://example.com/nextcloud/remote.php/dav/",
}),
);
});
});
@@ -0,0 +1,29 @@
import { describe, expect, test } from "vitest";
import { createNextcloudCalendarServerUrl } from "../nextcloud-url";
describe("createNextcloudCalendarServerUrl", () => {
test("uses the DAV endpoint for a root installation", () => {
expect(createNextcloudCalendarServerUrl("https://cloud.example.com")).toBe(
"https://cloud.example.com/remote.php/dav/",
);
});
test("preserves the base path for a subpath installation", () => {
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud")).toBe(
"https://example.com/nextcloud/remote.php/dav/",
);
});
test("does not duplicate a trailing slash", () => {
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/")).toBe(
"https://example.com/nextcloud/remote.php/dav/",
);
});
test("does not duplicate an existing DAV endpoint", () => {
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/remote.php/dav")).toBe(
"https://example.com/nextcloud/remote.php/dav/",
);
});
});
+1 -2
View File
@@ -6456,8 +6456,7 @@
},
"progress": {
"title": "Restoring database...",
"restarting": "Restarting server",
"relogin": "Restoring the database invalidated your current session. Homarr will send you to sign in when the server is ready."
"restarting": "Restarting server"
},
"timeout": {
"title": "Server did not come back online",