From c4c4d41e4d7c37d498ba25c91a734a9d7f66f233 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Tue, 6 Aug 2024 21:43:12 +0200 Subject: [PATCH] feat: add support for app url variables (#915) * feat: add support for app url variables * fix: test not working * fix: format issue --- .../src/app/[locale]/manage/apps/page.tsx | 5 ++-- packages/auth/redirect.ts | 13 ++++------- packages/common/package.json | 6 +++-- packages/common/src/app-url/base.ts | 23 +++++++++++++++++++ packages/common/src/app-url/client.ts | 5 ++++ packages/common/src/app-url/server.ts | 8 +++++++ packages/common/src/client.ts | 1 + packages/common/src/server.ts | 1 + packages/common/src/url.ts | 15 ++++++++++++ packages/widgets/src/app/component.tsx | 11 ++++++--- packages/widgets/src/app/serverData.ts | 5 +++- .../widgets/src/app/test/serverData.spec.ts | 3 +++ pnpm-lock.yaml | 16 +++++++++++++ 13 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 packages/common/src/app-url/base.ts create mode 100644 packages/common/src/app-url/client.ts create mode 100644 packages/common/src/app-url/server.ts create mode 100644 packages/common/src/client.ts diff --git a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx index 2e6983b6e..7ddf2816a 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx @@ -4,6 +4,7 @@ import { IconApps, IconPencil } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; +import { parseAppHrefWithVariablesServer } from "@homarr/common/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { ManageContainer } from "~/components/manage/manage-container"; @@ -69,8 +70,8 @@ const AppCard = async ({ app }: AppCardProps) => { )} {app.href && ( - - {app.href} + + {parseAppHrefWithVariablesServer(app.href)} )} diff --git a/packages/auth/redirect.ts b/packages/auth/redirect.ts index 4f3848981..94ed327a1 100644 --- a/packages/auth/redirect.ts +++ b/packages/auth/redirect.ts @@ -1,5 +1,7 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; +import { extractBaseUrlFromHeaders } from "@homarr/common"; + /** * The redirect_uri is constructed to work behind a reverse proxy. It is constructed from the headers x-forwarded-proto and x-forwarded-host. * @param headers @@ -11,16 +13,9 @@ export const createRedirectUri = (headers: ReadonlyHeaders | null, pathname: str return pathname; } - let protocol = headers.get("x-forwarded-proto") ?? "http"; - - // @see https://support.glitch.com/t/x-forwarded-proto-contains-multiple-protocols/17219 - if (protocol.includes(",")) { - protocol = protocol.includes("https") ? "https" : "http"; - } + const baseUrl = extractBaseUrlFromHeaders(headers); const path = pathname.startsWith("/") ? pathname : `/${pathname}`; - const host = headers.get("x-forwarded-host") ?? headers.get("host"); - - return `${protocol}://${host}${path}`; + return `${baseUrl}${path}`; }; diff --git a/packages/common/package.json b/packages/common/package.json index 100d1bea9..5d9f28f71 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -5,8 +5,9 @@ "type": "module", "exports": { ".": "./index.ts", + "./types": "./src/types.ts", "./server": "./src/server.ts", - "./types": "./src/types.ts" + "./client": "./src/client.ts" }, "typesVersions": { "*": { @@ -24,8 +25,9 @@ }, "dependencies": { "dayjs": "^1.11.12", + "next": "^14.2.5", "react": "^18.3.1", - "next": "^14.2.5" + "tldts": "^6.1.37" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/common/src/app-url/base.ts b/packages/common/src/app-url/base.ts new file mode 100644 index 000000000..b189c9352 --- /dev/null +++ b/packages/common/src/app-url/base.ts @@ -0,0 +1,23 @@ +import * as tldts from "tldts"; + +const safeParseTldts = (url: string) => { + try { + return tldts.parse(url); + } catch { + return null; + } +}; + +export const parseAppHrefWithVariables = (url: TInput, currentHref: string): TInput => { + if (!url || url.length === 0) return url; + + const tldtsResult = safeParseTldts(currentHref); + + const urlObject = new URL(currentHref); + + return url + .replaceAll("[homarr_base]", `${urlObject.protocol}//${urlObject.hostname}`) + .replaceAll("[homarr_hostname]", tldtsResult?.hostname ?? "") + .replaceAll("[homarr_domain]", tldtsResult?.domain ?? "") + .replaceAll("[homarr_protocol]", urlObject.protocol.replace(":", "")) as TInput; +}; diff --git a/packages/common/src/app-url/client.ts b/packages/common/src/app-url/client.ts new file mode 100644 index 000000000..e3c08268b --- /dev/null +++ b/packages/common/src/app-url/client.ts @@ -0,0 +1,5 @@ +import { parseAppHrefWithVariables } from "./base"; + +export const parseAppHrefWithVariablesClient = (url: TInput): TInput => { + return parseAppHrefWithVariables(url, window.location.href); +}; diff --git a/packages/common/src/app-url/server.ts b/packages/common/src/app-url/server.ts new file mode 100644 index 000000000..1f4ff363d --- /dev/null +++ b/packages/common/src/app-url/server.ts @@ -0,0 +1,8 @@ +import { headers } from "next/headers"; + +import { extractBaseUrlFromHeaders } from "../url"; +import { parseAppHrefWithVariables } from "./base"; + +export const parseAppHrefWithVariablesServer = (url: TInput): TInput => { + return parseAppHrefWithVariables(url, extractBaseUrlFromHeaders(headers())); +}; diff --git a/packages/common/src/client.ts b/packages/common/src/client.ts new file mode 100644 index 000000000..ce5720136 --- /dev/null +++ b/packages/common/src/client.ts @@ -0,0 +1 @@ +export * from "./app-url/client"; diff --git a/packages/common/src/server.ts b/packages/common/src/server.ts index 549972b93..8916238d4 100644 --- a/packages/common/src/server.ts +++ b/packages/common/src/server.ts @@ -1 +1,2 @@ +export * from "./app-url/server"; export * from "./security"; diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts index 5c46343ff..e734ae6e8 100644 --- a/packages/common/src/url.ts +++ b/packages/common/src/url.ts @@ -1,3 +1,5 @@ +import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; + export const appendPath = (url: URL | string, path: string) => { const newUrl = new URL(url); newUrl.pathname = removeTrailingSlash(newUrl.pathname) + path; @@ -7,3 +9,16 @@ export const appendPath = (url: URL | string, path: string) => { const removeTrailingSlash = (path: string) => { return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path; }; + +export const extractBaseUrlFromHeaders = (headers: ReadonlyHeaders): `${string}://${string}` => { + let protocol = headers.get("x-forwarded-proto") ?? "http"; + + // @see https://support.glitch.com/t/x-forwarded-proto-contains-multiple-protocols/17219 + if (protocol.includes(",")) { + protocol = protocol.includes("https") ? "https" : "http"; + } + + const host = headers.get("x-forwarded-host") ?? headers.get("host"); + + return `${protocol}://${host}`; +}; diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index bff9ad604..cf9422832 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -8,6 +8,7 @@ import combineClasses from "clsx"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; +import { parseAppHrefWithVariablesClient } from "@homarr/common/client"; import { useRegisterSpotlightActions } from "@homarr/spotlight"; import { useScopedI18n } from "@homarr/translation/client"; @@ -40,7 +41,7 @@ export default function AppWidget({ options, serverData, isEditMode, width }: Wi const shouldRunPing = Boolean(app?.href) && options.pingEnabled; clientApi.widget.app.updatedPing.useSubscription( - { url: app?.href ?? "" }, + { url: parseAppHrefWithVariablesClient(app?.href ?? "") }, { enabled: shouldRunPing, onData(data) { @@ -60,7 +61,7 @@ export default function AppWidget({ options, serverData, isEditMode, width }: Wi icon: app.iconUrl, group: "app", type: "link", - href: app.href, + href: parseAppHrefWithVariablesClient(app.href), openInNewTab: options.openInNewTab, }, ] @@ -92,7 +93,11 @@ export default function AppWidget({ options, serverData, isEditMode, width }: Wi } return ( - + let pingResult: RouterOutputs["widget"]["app"]["ping"] | null = null; if (app.href && options.pingEnabled) { - pingResult = await api.widget.app.ping({ url: app.href }); + pingResult = await api.widget.app.ping({ + url: parseAppHrefWithVariablesServer(app.href), + }); } return { app, pingResult }; diff --git a/packages/widgets/src/app/test/serverData.spec.ts b/packages/widgets/src/app/test/serverData.spec.ts index 2834eab58..999d7d1e8 100644 --- a/packages/widgets/src/app/test/serverData.spec.ts +++ b/packages/widgets/src/app/test/serverData.spec.ts @@ -30,6 +30,9 @@ vi.mock("@homarr/api/server", () => ({ }, }, })); +vi.mock("@homarr/common/server", () => ({ + parseAppHrefWithVariablesServer: () => "http://localhost", +})); describe("getServerDataAsync should load app and ping result", () => { test("when appId is empty it should return null for app and pingResult", async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efee6d049..cfac14700 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -636,6 +636,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + tldts: + specifier: ^6.1.37 + version: 6.1.37 devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -6174,6 +6177,13 @@ packages: title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + tldts-core@6.1.37: + resolution: {integrity: sha512-q6M/RBjZcUoF/KRhHFuGrcnaXLaXH8kHKH/e8XaAd9ULGYYhB32kr1ceIXR77a57OxRB/NR471BcYwU7jf4PAg==} + + tldts@6.1.37: + resolution: {integrity: sha512-QMvNTwl3b3vyweq158Cf+IeEWe/P1HVDULo5n7qnt70rzkU3Ya2amaWO36lX0C8w6X3l92fftcuHwLIX9QBkZg==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -11869,6 +11879,12 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + tldts-core@6.1.37: {} + + tldts@6.1.37: + dependencies: + tldts-core: 6.1.37 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2