mirror of
https://github.com/gethomepage/homepage.git
synced 2026-10-16 23:41:45 -07:00
Compare commits
1
Commits
v2.6.0
..
a6fcde2222
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6fcde2222 |
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
|
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
|
||||||
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7
|
uses: release-drafter/release-drafter@c345907820e0396f6f44bcfd9ac4763325f8feb0 # v7
|
||||||
with:
|
with:
|
||||||
config-name: release-drafter.yml
|
config-name: release-drafter.yml
|
||||||
version: ${{ github.event.inputs.version }}
|
version: ${{ github.event.inputs.version }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
|
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
|
||||||
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7
|
uses: release-drafter/release-drafter@c345907820e0396f6f44bcfd9ac4763325f8feb0 # v7
|
||||||
with:
|
with:
|
||||||
config-name: release-drafter.yml
|
config-name: release-drafter.yml
|
||||||
env:
|
env:
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter/autolabeler@978bb0b30c5ee8cdd332ced253758157150d43f0
|
- uses: release-drafter/release-drafter/autolabeler@c345907820e0396f6f44bcfd9ac4763325f8feb0
|
||||||
with:
|
with:
|
||||||
config-name: release-drafter.yml
|
config-name: release-drafter.yml
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
|
|
||||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A modern, <em>fast</em>, secure <em>fully proxied</em>, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.
|
A modern, <em>fully static, fast</em>, secure <em>fully proxied</em>, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
With features like quick search, bookmarks, weather support, a wide range of integrations and widgets, an elegant and modern design, and a focus on performance, Homepage is your ideal start to the day and a handy companion throughout it.
|
With features like quick search, bookmarks, weather support, a wide range of integrations and widgets, an elegant and modern design, and a focus on performance, Homepage is your ideal start to the day and a handy companion throughout it.
|
||||||
|
|
||||||
- **Fast** - Cached dashboard data and optimized assets keep page loads quick.
|
- **Fast** - The site is statically generated at build time for instant load times.
|
||||||
- **Secure** - All API requests to backend services are proxied, keeping your API keys hidden. Constantly reviewed for security by the community.
|
- **Secure** - All API requests to backend services are proxied, keeping your API keys hidden. Constantly reviewed for security by the community.
|
||||||
- **For Everyone** - Images built for AMD64, ARM64.
|
- **For Everyone** - Images built for AMD64, ARM64.
|
||||||
- **Full i18n** - Support for over 40 languages.
|
- **Full i18n** - Support for over 40 languages.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Settings
|
|||||||
description: Service Configuration
|
description: Service Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
The `settings.yaml` file allows you to define application level options. After making changes, click the refresh icon in the bottom right of the page to clear the cached dashboard data and reload the page.
|
The `settings.yaml` file allows you to define application level options. For changes made to this file to take effect, you will need to regenerate the static HTML, this can be done by clicking the refresh icon in the bottom right of the page.
|
||||||
|
|
||||||
## Title
|
## Title
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ startUrl: https://custom.url
|
|||||||
|
|
||||||
!!! warning "Heads Up!"
|
!!! warning "Heads Up!"
|
||||||
|
|
||||||
You will need to restart the container any time you add new local images so the server can discover them.
|
You will need to restart the container any time you add new images, this is a limitation of the Next.js static site server.
|
||||||
|
|
||||||
!!! warning "Heads Up!"
|
!!! warning "Heads Up!"
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Home
|
title: Home
|
||||||
description: A modern, fast, secure, fully proxied, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages.
|
description: A modern, fully static, fast, secure, fully proxied, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages.
|
||||||
icon: material/home
|
icon: material/home
|
||||||
hide:
|
hide:
|
||||||
- navigation
|
- navigation
|
||||||
@@ -18,7 +18,7 @@ hide:
|
|||||||
|
|
||||||
<img src="assets/homepage_demo_clip.webp" alt="homepage" style="max-width: 100%; margin: 0 auto; display: block;" />
|
<img src="assets/homepage_demo_clip.webp" alt="homepage" style="max-width: 100%; margin: 0 auto; display: block;" />
|
||||||
|
|
||||||
<p style="margin: 0 0 30px;">A modern, <em>fast</em>, secure <em>fully proxied</em>, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.</p>
|
<p style="margin: 0 0 30px;">A modern, <em>fully static, fast</em>, secure <em>fully proxied</em>, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ Finally, run the server:
|
|||||||
HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev:1234 pnpm start
|
HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev:1234 pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
When updating Homepage versions, rebuild the production bundle by repeating the steps above.
|
When updating homepage versions you will need to re-build the static files i.e. repeat the process above.
|
||||||
|
|
||||||
See [HOMEPAGE_ALLOWED_HOSTS](index.md#homepage_allowed_hosts) for more information on this environment variable.
|
See [HOMEPAGE_ALLOWED_HOSTS](index.md#homepage_allowed_hosts) for more information on this environment variable.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "2.6.0",
|
"version": "2.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
|||||||
@@ -791,7 +791,7 @@
|
|||||||
"processing": "Behandler",
|
"processing": "Behandler",
|
||||||
"processed": "Behandlet",
|
"processed": "Behandlet",
|
||||||
"time": "Tid",
|
"time": "Tid",
|
||||||
"failed": "Fejlet"
|
"failed": "Failed"
|
||||||
},
|
},
|
||||||
"firefly": {
|
"firefly": {
|
||||||
"networth": "Nettoformue",
|
"networth": "Nettoformue",
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import createMockRes from "test-utils/create-mock-res";
|
|
||||||
|
|
||||||
vi.mock("next-auth/jwt", () => ({ getToken: vi.fn(async () => null) }));
|
|
||||||
vi.mock("utils/logger", () => ({ default: () => ({ error: vi.fn(), warn: vi.fn(), debug: vi.fn(), info: vi.fn() }) }));
|
|
||||||
|
|
||||||
const allRoutes = Object.entries(import.meta.glob("/src/pages/api/**/*.js"));
|
|
||||||
const routes = allRoutes.filter(([file]) => !/\/(auth|mcp)\//.test(file) && !file.endsWith("/healthcheck.js"));
|
|
||||||
|
|
||||||
describe("API authentication without middleware", () => {
|
|
||||||
beforeEach(() => vi.stubEnv("HOMEPAGE_AUTH_ENABLED", "true"));
|
|
||||||
afterEach(() => vi.unstubAllEnvs());
|
|
||||||
|
|
||||||
it.each(routes)("rejects anonymous access to %s at its exported handler", async (_file, load) => {
|
|
||||||
const { default: handler } = await load();
|
|
||||||
const req = { query: {}, headers: { "x-prerender-revalidate": "public-image-key" }, cookies: {} };
|
|
||||||
const res = createMockRes();
|
|
||||||
await handler(req, res);
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
expect(res.body).toEqual({ error: "Unauthorized" });
|
|
||||||
expect(res.headers["Cache-Control"]).toBe("private, no-store");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps sign-in CSS public while protecting custom JavaScript", async () => {
|
|
||||||
const { default: handler } = await import("pages/api/config/[path]");
|
|
||||||
const css = createMockRes();
|
|
||||||
await handler({ query: { path: "custom.css" } }, css);
|
|
||||||
expect(css.statusCode).toBe(200);
|
|
||||||
const js = createMockRes();
|
|
||||||
await handler({ query: { path: "custom.js" }, headers: {}, cookies: {} }, js);
|
|
||||||
expect(js.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("API host validation without middleware", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.stubEnv("HOMEPAGE_ALLOWED_HOSTS", "");
|
|
||||||
vi.stubEnv("PORT", "3000");
|
|
||||||
});
|
|
||||||
afterEach(() => vi.unstubAllEnvs());
|
|
||||||
|
|
||||||
it.each(allRoutes)("rejects disallowed hosts at %s, including public routes", async (_file, load) => {
|
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
const { default: handler } = await load();
|
|
||||||
const req = {
|
|
||||||
query: { path: "custom.css", nextauth: ["session"] },
|
|
||||||
method: "POST",
|
|
||||||
headers: { host: "evil.example", "x-prerender-revalidate": "public-image-key" },
|
|
||||||
cookies: {},
|
|
||||||
};
|
|
||||||
const res = createMockRes();
|
|
||||||
await handler(req, res);
|
|
||||||
expect(res.statusCode).toBe(400);
|
|
||||||
expect(res.body).toEqual({ error: "Host validation failed. See logs for more details." });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,20 +3,27 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import createMockRes from "test-utils/create-mock-res";
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
import handler from "pages/api/revalidate";
|
import handler from "pages/api/revalidate";
|
||||||
import { invalidateDashboardData } from "utils/config/dashboard";
|
|
||||||
|
|
||||||
vi.mock("utils/config/dashboard", () => ({ invalidateDashboardData: vi.fn() }));
|
|
||||||
|
|
||||||
describe("pages/api/revalidate", () => {
|
describe("pages/api/revalidate", () => {
|
||||||
it("invalidates the dashboard snapshot without regenerating a static page", async () => {
|
it("revalidates and returns {revalidated:true}", async () => {
|
||||||
const req = {};
|
const req = {};
|
||||||
const res = createMockRes();
|
const res = createMockRes();
|
||||||
res.revalidate = vi.fn().mockResolvedValueOnce(undefined);
|
res.revalidate = vi.fn().mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(invalidateDashboardData).toHaveBeenCalledOnce();
|
expect(res.revalidate).toHaveBeenCalledWith("/");
|
||||||
expect(res.revalidate).not.toHaveBeenCalled();
|
|
||||||
expect(res.body).toEqual({ revalidated: true });
|
expect(res.body).toEqual({ revalidated: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 500 when revalidate throws", async () => {
|
||||||
|
const req = {};
|
||||||
|
const res = createMockRes();
|
||||||
|
res.revalidate = vi.fn().mockRejectedValueOnce(new Error("nope"));
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(500);
|
||||||
|
expect(res.body).toBe("Error revalidating");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,22 +19,22 @@ describe("pages/api/theme", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns defaults when settings are missing", async () => {
|
it("returns defaults when settings are missing", () => {
|
||||||
getSettings.mockReturnValueOnce({});
|
getSettings.mockReturnValueOnce({});
|
||||||
|
|
||||||
const res = createMockRes();
|
const res = createMockRes();
|
||||||
await handler({}, res);
|
handler({ res });
|
||||||
|
|
||||||
expect(checkAndCopyConfig).toHaveBeenCalledWith("settings.yaml");
|
expect(checkAndCopyConfig).toHaveBeenCalledWith("settings.yaml");
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
expect(res.body).toEqual({ color: "slate", theme: "dark" });
|
expect(res.body).toEqual({ color: "slate", theme: "dark" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns configured color + theme when present", async () => {
|
it("returns configured color + theme when present", () => {
|
||||||
getSettings.mockReturnValueOnce({ color: "red", theme: "light" });
|
getSettings.mockReturnValueOnce({ color: "red", theme: "light" });
|
||||||
|
|
||||||
const res = createMockRes();
|
const res = createMockRes();
|
||||||
await handler({}, res);
|
handler({ res });
|
||||||
|
|
||||||
expect(res.body).toEqual({ color: "red", theme: "light" });
|
expect(res.body).toEqual({ color: "red", theme: "light" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { invalidateDashboardData } from "utils/config/dashboard";
|
|
||||||
import { ColorContext } from "utils/contexts/color";
|
import { ColorContext } from "utils/contexts/color";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
import { TabContext } from "utils/contexts/tab";
|
import { TabContext } from "utils/contexts/tab";
|
||||||
@@ -171,9 +170,8 @@ vi.mock("components/toggles/revalidate", () => ({
|
|||||||
default: () => null,
|
default: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("pages/index getServerSideProps", () => {
|
describe("pages/index getStaticProps", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
invalidateDashboardData();
|
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
state.throwIn = null;
|
state.throwIn = null;
|
||||||
state.validateData = [];
|
state.validateData = [];
|
||||||
@@ -192,8 +190,8 @@ describe("pages/index getServerSideProps", () => {
|
|||||||
it("returns initial settings and api fallbacks for swr", async () => {
|
it("returns initial settings and api fallbacks for swr", async () => {
|
||||||
getSettings.mockReturnValueOnce({ providers: { x: 1 }, language: "en", title: "Homepage" });
|
getSettings.mockReturnValueOnce({ providers: { x: 1 }, language: "en", title: "Homepage" });
|
||||||
|
|
||||||
const { getServerSideProps } = await import("pages/index.jsx");
|
const { getStaticProps } = await import("pages/index.jsx");
|
||||||
const result = await getServerSideProps({});
|
const result = await getStaticProps();
|
||||||
|
|
||||||
expect(result.props.initialSettings).toEqual({ language: "en", title: "Homepage" });
|
expect(result.props.initialSettings).toEqual({ language: "en", title: "Homepage" });
|
||||||
expect(result.props.fallback["/api/services"]).toEqual([{ name: "svc" }]);
|
expect(result.props.fallback["/api/services"]).toEqual([{ name: "svc" }]);
|
||||||
@@ -206,8 +204,8 @@ describe("pages/index getServerSideProps", () => {
|
|||||||
it("normalizes legacy language codes before requesting translations", async () => {
|
it("normalizes legacy language codes before requesting translations", async () => {
|
||||||
getSettings.mockReturnValueOnce({ providers: {}, language: "zh-CN" });
|
getSettings.mockReturnValueOnce({ providers: {}, language: "zh-CN" });
|
||||||
|
|
||||||
const { getServerSideProps } = await import("pages/index.jsx");
|
const { getStaticProps } = await import("pages/index.jsx");
|
||||||
await getServerSideProps({});
|
await getStaticProps();
|
||||||
|
|
||||||
expect(serverSideTranslations).toHaveBeenCalledWith("zh-Hans");
|
expect(serverSideTranslations).toHaveBeenCalledWith("zh-Hans");
|
||||||
});
|
});
|
||||||
@@ -216,8 +214,8 @@ describe("pages/index getServerSideProps", () => {
|
|||||||
getSettings.mockReturnValueOnce({ providers: {}, language: "de" });
|
getSettings.mockReturnValueOnce({ providers: {}, language: "de" });
|
||||||
state.throwIn = "services";
|
state.throwIn = "services";
|
||||||
|
|
||||||
const { getServerSideProps } = await import("pages/index.jsx");
|
const { getStaticProps } = await import("pages/index.jsx");
|
||||||
const result = await getServerSideProps({});
|
const result = await getStaticProps();
|
||||||
|
|
||||||
expect(result.props.initialSettings).toEqual({});
|
expect(result.props.initialSettings).toEqual({});
|
||||||
expect(result.props.fallback["/api/services"]).toEqual([]);
|
expect(result.props.fallback["/api/services"]).toEqual([]);
|
||||||
|
|||||||
+13
-3
@@ -1,7 +1,6 @@
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { isAllowedHost, secureCookie } from "utils/auth";
|
|
||||||
import { isAuthEnabled } from "utils/env";
|
import { isAuthEnabled } from "utils/env";
|
||||||
|
|
||||||
const authEnabled = isAuthEnabled();
|
const authEnabled = isAuthEnabled();
|
||||||
@@ -18,7 +17,18 @@ function withPrivateCache(res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function middleware(req) {
|
export async function middleware(req) {
|
||||||
if (!isAllowedHost(req.headers.get("host"))) {
|
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
|
||||||
|
const host = req.headers.get("host");
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
|
||||||
|
const allowAll = process.env.HOMEPAGE_ALLOWED_HOSTS === "*";
|
||||||
|
if (process.env.HOMEPAGE_ALLOWED_HOSTS) {
|
||||||
|
allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(","));
|
||||||
|
}
|
||||||
|
if (!allowAll && (!host || !allowedHosts.includes(host))) {
|
||||||
|
console.error(
|
||||||
|
`Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`,
|
||||||
|
);
|
||||||
return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 });
|
return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +40,7 @@ export async function middleware(req) {
|
|||||||
return withPrivateCache(NextResponse.next());
|
return withPrivateCache(NextResponse.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getToken({ req, secret: authSecret, secureCookie: secureCookie() });
|
const token = await getToken({ req, secret: authSecret });
|
||||||
if (!token) {
|
if (!token) {
|
||||||
const signInUrl = new URL("/auth/signin", req.url);
|
const signInUrl = new URL("/auth/signin", req.url);
|
||||||
// Same-origin by construction, so this cannot be used as an open redirect
|
// Same-origin by construction, so this cannot be used as an open redirect
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ describe("middleware", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
process.env = { ...originalEnv };
|
process.env = { ...originalEnv };
|
||||||
delete process.env.HOMEPAGE_ALLOWED_HOSTS;
|
|
||||||
console.error = originalConsoleError;
|
console.error = originalConsoleError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,29 +135,6 @@ describe("middleware", () => {
|
|||||||
expect(res.type).toBe("next");
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
|
||||||
["NEXTAUTH_URL", "https://homepage.example", true],
|
|
||||||
["NEXTAUTH_URL", "HTTPS://homepage.example", true],
|
|
||||||
["NEXTAUTH_URL", " https://homepage.example ", true],
|
|
||||||
["NEXTAUTH_URL", "HTTP://homepage.example", false],
|
|
||||||
["HOMEPAGE_EXTERNAL_URL", "HTTPS://homepage.example", true],
|
|
||||||
["HOMEPAGE_EXTERNAL_URL", " https://homepage.example ", true],
|
|
||||||
["HOMEPAGE_EXTERNAL_URL", "homepage.example", false],
|
|
||||||
])("reads the session cookie NextAuth sets when %s is %j", async (variable, url, secure) => {
|
|
||||||
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
|
||||||
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
|
||||||
delete process.env.NEXTAUTH_URL;
|
|
||||||
process.env[variable] = url;
|
|
||||||
|
|
||||||
getToken.mockResolvedValueOnce({ sub: "user" });
|
|
||||||
|
|
||||||
const middleware = await loadMiddleware();
|
|
||||||
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/"));
|
|
||||||
|
|
||||||
expect(getToken).toHaveBeenCalledWith(expect.objectContaining({ secureCookie: secure }));
|
|
||||||
expect(res.type).toBe("next");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("redirects to signin when auth is enabled and no token is present", async () => {
|
it("redirects to signin when auth is enabled and no token is present", async () => {
|
||||||
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
@@ -171,7 +147,6 @@ describe("middleware", () => {
|
|||||||
expect(getToken).toHaveBeenCalledWith({
|
expect(getToken).toHaveBeenCalledWith({
|
||||||
req: expect.objectContaining({ url: "http://localhost:3000/some" }),
|
req: expect.objectContaining({ url: "http://localhost:3000/some" }),
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
secureCookie: false,
|
|
||||||
});
|
});
|
||||||
expect(NextResponse.redirect).toHaveBeenCalled();
|
expect(NextResponse.redirect).toHaveBeenCalled();
|
||||||
expect(res.type).toBe("redirect");
|
expect(res.type).toBe("redirect");
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
|
||||||
import { withAllowedHost } from "utils/auth";
|
|
||||||
import { applyNextAuthEnv, isAuthEnabled } from "utils/env";
|
import { applyNextAuthEnv, isAuthEnabled } from "utils/env";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
@@ -164,7 +163,7 @@ export const authOptions = {
|
|||||||
|
|
||||||
const nextAuthHandler = NextAuth(authOptions);
|
const nextAuthHandler = NextAuth(authOptions);
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
// Just pass empty session if auth not enabled
|
// Just pass empty session if auth not enabled
|
||||||
if (!authEnabled) {
|
if (!authEnabled) {
|
||||||
return res.status(200).json({});
|
return res.status(200).json({});
|
||||||
@@ -172,5 +171,3 @@ async function handler(req, res) {
|
|||||||
|
|
||||||
return nextAuthHandler(req, res);
|
return nextAuthHandler(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAllowedHost(handler);
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { bookmarksResponse } from "utils/config/api-response";
|
import { bookmarksResponse } from "utils/config/api-response";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.send(await bookmarksResponse());
|
res.send(await bookmarksResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { CONF_DIR } from "utils/config/config";
|
import { CONF_DIR } from "utils/config/config";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ const logger = createLogger("configFileService");
|
|||||||
* @param {import("next").NextApiRequest} req
|
* @param {import("next").NextApiRequest} req
|
||||||
* @param {import("next").NextApiResponse} res
|
* @param {import("next").NextApiResponse} res
|
||||||
*/
|
*/
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { path: relativePath } = req.query;
|
const { path: relativePath } = req.query;
|
||||||
|
|
||||||
// only two supported files, for now
|
// only two supported files, for now
|
||||||
@@ -33,5 +32,3 @@ async function handler(req, res) {
|
|||||||
return res.status(500).end("Internal Server Error");
|
return res.status(500).end("Internal Server Error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler, { public: (req) => req.query?.path === "custom.css" });
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getDockerStats } from "utils/docker/stats";
|
import { getDockerStats } from "utils/docker/stats";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("dockerStats");
|
const logger = createLogger("dockerStats");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
const result = await getDockerStats(req.query.server);
|
const result = await getDockerStats(req.query.server);
|
||||||
|
|
||||||
@@ -20,5 +19,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getDockerStatuses } from "utils/docker/status";
|
import { getDockerStatuses } from "utils/docker/status";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("dockerStatuses");
|
const logger = createLogger("dockerStatuses");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
const result = await getDockerStatuses(req.query.server);
|
const result = await getDockerStatuses(req.query.server);
|
||||||
|
|
||||||
@@ -20,5 +19,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { createHash } from "crypto";
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
|
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
|
||||||
|
|
||||||
const configs = [
|
const configs = [
|
||||||
@@ -21,7 +20,7 @@ function hash(buffer) {
|
|||||||
return hashSum.digest("hex");
|
return hashSum.digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const hashes = configs.map((config) => {
|
const hashes = configs.map((config) => {
|
||||||
checkAndCopyConfig(config);
|
checkAndCopyConfig(config);
|
||||||
const configYaml = join(CONF_DIR, config);
|
const configYaml = join(CONF_DIR, config);
|
||||||
@@ -37,5 +36,3 @@ async function handler(req, res) {
|
|||||||
hash: combinedHash,
|
hash: combinedHash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { withAllowedHost } from "utils/auth";
|
export default function handler(req, res) {
|
||||||
|
|
||||||
function handler(req, res) {
|
|
||||||
res.send("up");
|
res.send("up");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAllowedHost(handler);
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { getKubeConfig } from "../../../../utils/config/kubernetes";
|
|||||||
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/utils";
|
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/utils";
|
||||||
import createLogger from "../../../../utils/logger";
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
|
|
||||||
const logger = createLogger("kubernetesStatsService");
|
const logger = createLogger("kubernetesStatsService");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const APP_LABEL = "app.kubernetes.io/name";
|
const APP_LABEL = "app.kubernetes.io/name";
|
||||||
const { service, podSelector } = req.query;
|
const { service, podSelector } = req.query;
|
||||||
|
|
||||||
@@ -110,5 +108,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { CoreV1Api } from "@kubernetes/client-node";
|
|||||||
import { getKubeConfig } from "../../../../utils/config/kubernetes";
|
import { getKubeConfig } from "../../../../utils/config/kubernetes";
|
||||||
import createLogger from "../../../../utils/logger";
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
|
|
||||||
const logger = createLogger("kubernetesStatusService");
|
const logger = createLogger("kubernetesStatusService");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const APP_LABEL = "app.kubernetes.io/name";
|
const APP_LABEL = "app.kubernetes.io/name";
|
||||||
const { service, podSelector } = req.query;
|
const { service, podSelector } = req.query;
|
||||||
|
|
||||||
@@ -70,5 +68,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getServerSession } from "next-auth/next";
|
import { getServerSession } from "next-auth/next";
|
||||||
|
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]";
|
import { authOptions } from "pages/api/auth/[...nextauth]";
|
||||||
import { withAllowedHost } from "utils/auth";
|
|
||||||
import { isAuthEnabled } from "utils/env";
|
import { isAuthEnabled } from "utils/env";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { handleMcpRequest, mcpEnabled, mcpTokenAuthorized, mcpTokenConfigError } from "utils/mcp/homepage-mcp";
|
import { handleMcpRequest, mcpEnabled, mcpTokenAuthorized, mcpTokenConfigError } from "utils/mcp/homepage-mcp";
|
||||||
@@ -11,8 +10,7 @@ async function hasHomepageSession(req, res) {
|
|||||||
return Boolean(await getServerSession(req, res, authOptions));
|
return Boolean(await getServerSession(req, res, authOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.setHeader("Cache-Control", "private, no-store");
|
|
||||||
if (!mcpEnabled()) {
|
if (!mcpEnabled()) {
|
||||||
return res.status(404).end("Not Found");
|
return res.status(404).end("Not Found");
|
||||||
}
|
}
|
||||||
@@ -40,5 +38,3 @@ async function handler(req, res) {
|
|||||||
|
|
||||||
return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAllowedHost(handler);
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { promise as ping } from "ping";
|
import { promise as ping } from "ping";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getServiceItem } from "utils/config/service-helpers";
|
import { getServiceItem } from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("ping");
|
const logger = createLogger("ping");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { groupName, serviceName } = req.query;
|
const { groupName, serviceName } = req.query;
|
||||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||||
if (!serviceItem) {
|
if (!serviceItem) {
|
||||||
@@ -41,5 +40,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getProxmoxConfig } from "utils/config/proxmox";
|
import { getProxmoxConfig } from "utils/config/proxmox";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
import { httpProxy } from "utils/proxy/http";
|
||||||
@@ -8,7 +7,7 @@ const VALID_VM_TYPES = new Set(["qemu", "lxc"]);
|
|||||||
const VALID_NODE = /^[A-Za-z0-9._-]+$/;
|
const VALID_NODE = /^[A-Za-z0-9._-]+$/;
|
||||||
const VALID_VMID = /^\d+$/;
|
const VALID_VMID = /^\d+$/;
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { service, type: vmType } = req.query;
|
const { service, type: vmType } = req.query;
|
||||||
|
|
||||||
const [node, vmid] = service || [];
|
const [node, vmid] = service || [];
|
||||||
@@ -104,5 +103,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
const logger = createLogger("releases");
|
const logger = createLogger("releases");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases";
|
const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases";
|
||||||
try {
|
try {
|
||||||
return res.send(await cachedRequest(releasesURL, 5));
|
return res.send(await cachedRequest(releasesURL, 5));
|
||||||
@@ -13,5 +12,3 @@ async function handler(req, res) {
|
|||||||
return res.send([]);
|
return res.send([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { withAuth } from "utils/auth";
|
export default async function handler(req, res) {
|
||||||
import { invalidateDashboardData } from "utils/config/dashboard";
|
try {
|
||||||
|
await res.revalidate("/");
|
||||||
async function handler(req, res) {
|
|
||||||
invalidateDashboardData();
|
|
||||||
return res.json({ revalidated: true });
|
return res.json({ revalidated: true });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).send("Error revalidating");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { searchProviders } from "components/widgets/search/search";
|
import { searchProviders } from "components/widgets/search/search";
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import { widgetsFromConfig } from "utils/config/widget-helpers";
|
import { widgetsFromConfig } from "utils/config/widget-helpers";
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { query, providerName } = req.query;
|
const { query, providerName } = req.query;
|
||||||
|
|
||||||
const provider = Object.values(searchProviders).find(({ name }) => name === providerName);
|
const provider = Object.values(searchProviders).find(({ name }) => name === providerName);
|
||||||
@@ -35,5 +34,3 @@ async function handler(req, res) {
|
|||||||
|
|
||||||
return res.send(await cachedRequest(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0"));
|
return res.send(await cachedRequest(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { servicesResponse } from "utils/config/api-response";
|
import { servicesResponse } from "utils/config/api-response";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.send(await servicesResponse());
|
res.send(await servicesResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import getServiceWidget from "utils/config/service-helpers";
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
@@ -42,7 +41,7 @@ function getSafeSegments(rawSegments, allowedSegments) {
|
|||||||
return safeSegments;
|
return safeSegments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
const { service, group, index } = req.query;
|
const { service, group, index } = req.query;
|
||||||
const serviceWidget = await getServiceWidget(group, service, index);
|
const serviceWidget = await getServiceWidget(group, service, index);
|
||||||
@@ -145,5 +144,3 @@ async function handler(req, res) {
|
|||||||
return res.status(500).send({ error: "Unexpected error" });
|
return res.status(500).send({ error: "Unexpected error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { performance } from "perf_hooks";
|
import { performance } from "perf_hooks";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getServiceItem } from "utils/config/service-helpers";
|
import { getServiceItem } from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
const logger = createLogger("siteMonitor");
|
const logger = createLogger("siteMonitor");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { groupName, serviceName } = req.query;
|
const { groupName, serviceName } = req.query;
|
||||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||||
if (!serviceItem) {
|
if (!serviceItem) {
|
||||||
@@ -51,5 +50,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import checkAndCopyConfig, { getSettings } from "utils/config/config";
|
import checkAndCopyConfig, { getSettings } from "utils/config/config";
|
||||||
|
|
||||||
function handler(req, res) {
|
export default function handler({ res }) {
|
||||||
checkAndCopyConfig("settings.yaml");
|
checkAndCopyConfig("settings.yaml");
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -13,5 +12,3 @@ function handler(req, res) {
|
|||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import checkAndCopyConfig from "utils/config/config";
|
import checkAndCopyConfig from "utils/config/config";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "kubernetes.yaml", "proxmox.yaml"];
|
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "kubernetes.yaml", "proxmox.yaml"];
|
||||||
const logger = createLogger("configValidationHandler");
|
const logger = createLogger("configValidationHandler");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
let errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
let errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
logger.error("Configuration validation errors", errors);
|
logger.error("Configuration validation errors", errors);
|
||||||
@@ -18,5 +17,3 @@ async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
res.send(errors);
|
res.send(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { sanitizeErrorURL } from "utils/proxy/api-helpers";
|
import { sanitizeErrorURL } from "utils/proxy/api-helpers";
|
||||||
@@ -6,7 +5,7 @@ import { httpProxy } from "utils/proxy/http";
|
|||||||
|
|
||||||
const logger = createLogger("customapi");
|
const logger = createLogger("customapi");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { index } = req.query;
|
const { index } = req.query;
|
||||||
|
|
||||||
const options = await getPrivateWidgetOptions("customapi", index);
|
const options = await getPrivateWidgetOptions("customapi", index);
|
||||||
@@ -45,5 +44,3 @@ async function handler(req, res) {
|
|||||||
return res.status(500).json({ error: { message: "Invalid JSON", url: sanitizeErrorURL(url) } });
|
return res.status(500).json({ error: { message: "Invalid JSON", url: sanitizeErrorURL(url) } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { parseVersionForUrl } from "utils/proxy/api-helpers";
|
import { parseVersionForUrl } from "utils/proxy/api-helpers";
|
||||||
@@ -43,7 +42,7 @@ async function retrieveFromGlancesAPI(privateWidgetOptions, endpoint) {
|
|||||||
return JSON.parse(Buffer.from(data).toString());
|
return JSON.parse(Buffer.from(data).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks, version } = req.query;
|
const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks, version } = req.query;
|
||||||
|
|
||||||
const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
|
const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
|
||||||
@@ -77,5 +76,3 @@ async function handler(req, res) {
|
|||||||
return res.status(400).json({ error: e.message });
|
return res.status(400).json({ error: e.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { widgetsResponse } from "utils/config/api-response";
|
import { widgetsResponse } from "utils/config/api-response";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.send(await widgetsResponse());
|
res.send(await widgetsResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { getKubeConfig } from "../../../utils/config/kubernetes";
|
|||||||
import { parseCpu, parseMemory } from "../../../utils/kubernetes/utils";
|
import { parseCpu, parseMemory } from "../../../utils/kubernetes/utils";
|
||||||
import createLogger from "../../../utils/logger";
|
import createLogger from "../../../utils/logger";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
|
|
||||||
const logger = createLogger("widget");
|
const logger = createLogger("widget");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
const kc = getKubeConfig();
|
const kc = getKubeConfig();
|
||||||
if (!kc) {
|
if (!kc) {
|
||||||
@@ -100,5 +98,3 @@ async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { getSettings } from "../../../utils/config/config";
|
|||||||
import createLogger from "../../../utils/logger";
|
import createLogger from "../../../utils/logger";
|
||||||
import { httpProxy } from "../../../utils/proxy/http";
|
import { httpProxy } from "../../../utils/proxy/http";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
|
|
||||||
const logger = createLogger("longhorn");
|
const logger = createLogger("longhorn");
|
||||||
|
|
||||||
function parseLonghornData(data) {
|
function parseLonghornData(data) {
|
||||||
@@ -49,7 +47,7 @@ function parseLonghornData(data) {
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const longhornSettings = settings?.providers?.longhorn || {};
|
const longhornSettings = settings?.providers?.longhorn || {};
|
||||||
const { url, username, password } = longhornSettings;
|
const { url, username, password } = longhornSettings;
|
||||||
@@ -87,5 +85,3 @@ async function handler(req, res) {
|
|||||||
nodes,
|
nodes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, units, cache, timezone } = req.query;
|
const { latitude, longitude, units, cache, timezone } = req.query;
|
||||||
const degrees = units === "metric" ? "celsius" : "fahrenheit";
|
const degrees = units === "metric" ? "celsius" : "fahrenheit";
|
||||||
const timezeone = timezone ?? "auto";
|
const timezeone = timezone ?? "auto";
|
||||||
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
|
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
|
||||||
return res.send(await cachedRequest(apiUrl, cache));
|
return res.send(await cachedRequest(apiUrl, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, units, provider, cache, lang, index } = req.query;
|
const { latitude, longitude, units, provider, cache, lang, index } = req.query;
|
||||||
const privateWidgetOptions = await getPrivateWidgetOptions("openweathermap", index);
|
const privateWidgetOptions = await getPrivateWidgetOptions("openweathermap", index);
|
||||||
let { apiKey } = privateWidgetOptions;
|
let { apiKey } = privateWidgetOptions;
|
||||||
@@ -29,5 +28,3 @@ async function handler(req, res) {
|
|||||||
|
|
||||||
return res.send(await cachedRequest(apiUrl, cache));
|
return res.send(await cachedRequest(apiUrl, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import si from "systeminformation";
|
import si from "systeminformation";
|
||||||
|
|
||||||
import { withAuth } from "utils/auth";
|
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("resources");
|
const logger = createLogger("resources");
|
||||||
@@ -20,7 +19,7 @@ function isMissingNetworkStat(networkData, interfaceName) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { type, target, interfaceName = "default" } = req.query;
|
const { type, target, interfaceName = "default" } = req.query;
|
||||||
|
|
||||||
if (type === "cpu") {
|
if (type === "cpu") {
|
||||||
@@ -115,5 +114,3 @@ async function handler(req, res) {
|
|||||||
error: "invalid type",
|
error: "invalid type",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
const logger = createLogger("stocks");
|
const logger = createLogger("stocks");
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { watchlist, provider, cache } = req.query;
|
const { watchlist, provider, cache } = req.query;
|
||||||
|
|
||||||
logger.debug("Stocks API request: %o", { watchlist, provider, cache });
|
logger.debug("Stocks API request: %o", { watchlist, provider, cache });
|
||||||
@@ -82,5 +81,3 @@ async function handler(req, res) {
|
|||||||
/* c8 ignore next 2 -- provider validation above currently makes this unreachable */
|
/* c8 ignore next 2 -- provider validation above currently makes this unreachable */
|
||||||
return res.status(400).json({ error: "Invalid configuration" });
|
return res.status(400).json({ error: "Invalid configuration" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { withAuth } from "utils/auth";
|
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
import { cachedRequest } from "utils/proxy/http";
|
import { cachedRequest } from "utils/proxy/http";
|
||||||
|
|
||||||
async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, provider, cache, lang, index } = req.query;
|
const { latitude, longitude, provider, cache, lang, index } = req.query;
|
||||||
const privateWidgetOptions = await getPrivateWidgetOptions("weatherapi", index);
|
const privateWidgetOptions = await getPrivateWidgetOptions("weatherapi", index);
|
||||||
let { apiKey } = privateWidgetOptions;
|
let { apiKey } = privateWidgetOptions;
|
||||||
@@ -29,5 +28,3 @@ async function handler(req, res) {
|
|||||||
|
|
||||||
return res.send(await cachedRequest(apiUrl, cache));
|
return res.send(await cachedRequest(apiUrl, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuth(handler);
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { withPageAuth } from "utils/auth";
|
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import themes from "utils/styles/themes";
|
import themes from "utils/styles/themes";
|
||||||
|
|
||||||
async function getProps({ res }) {
|
export async function getServerSideProps({ res }) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const color = settings.color || "slate";
|
const color = settings.color || "slate";
|
||||||
@@ -28,5 +27,3 @@ async function getProps({ res }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BrowserConfig() {}
|
export default function BrowserConfig() {}
|
||||||
|
|
||||||
export const getServerSideProps = withPageAuth(getProps);
|
|
||||||
|
|||||||
+45
-3
@@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
|
import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -15,13 +16,14 @@ import ServicesGroup from "components/services/group";
|
|||||||
import Tab, { slugifyAndEncode } from "components/tab";
|
import Tab, { slugifyAndEncode } from "components/tab";
|
||||||
import Revalidate from "components/toggles/revalidate";
|
import Revalidate from "components/toggles/revalidate";
|
||||||
import Widget from "components/widgets/widget";
|
import Widget from "components/widgets/widget";
|
||||||
import { withPageAuth } from "utils/auth";
|
import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
|
||||||
import { getDashboardData } from "utils/config/dashboard";
|
import { getSettings } from "utils/config/config";
|
||||||
import { ColorContext } from "utils/contexts/color";
|
import { ColorContext } from "utils/contexts/color";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
import { TabContext } from "utils/contexts/tab";
|
import { TabContext } from "utils/contexts/tab";
|
||||||
import { ThemeContext } from "utils/contexts/theme";
|
import { ThemeContext } from "utils/contexts/theme";
|
||||||
import useWindowFocus from "utils/hooks/window-focus";
|
import useWindowFocus from "utils/hooks/window-focus";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
import themes from "utils/styles/themes";
|
import themes from "utils/styles/themes";
|
||||||
|
|
||||||
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
|
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
|
||||||
@@ -53,7 +55,47 @@ const normalizeLanguage = (language) => {
|
|||||||
return alias || language;
|
return alias || language;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = withPageAuth(async () => ({ props: await getDashboardData() }));
|
export async function getStaticProps() {
|
||||||
|
let logger;
|
||||||
|
try {
|
||||||
|
logger = createLogger("index");
|
||||||
|
const { providers, ...settings } = getSettings();
|
||||||
|
|
||||||
|
const services = await servicesResponse();
|
||||||
|
const bookmarks = await bookmarksResponse();
|
||||||
|
const widgets = await widgetsResponse();
|
||||||
|
const language = normalizeLanguage(settings.language);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
initialSettings: settings,
|
||||||
|
fallback: {
|
||||||
|
"/api/services": services,
|
||||||
|
"/api/bookmarks": bookmarks,
|
||||||
|
"/api/widgets": widgets,
|
||||||
|
"/api/hash": false,
|
||||||
|
},
|
||||||
|
...(await serverSideTranslations(language)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (logger && e) {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
initialSettings: {},
|
||||||
|
fallback: {
|
||||||
|
"/api/services": [],
|
||||||
|
"/api/bookmarks": [],
|
||||||
|
"/api/widgets": [],
|
||||||
|
"/api/hash": false,
|
||||||
|
},
|
||||||
|
...(await serverSideTranslations("en")),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Index({ initialSettings, fallback }) {
|
function Index({ initialSettings, fallback }) {
|
||||||
const windowFocused = useWindowFocus();
|
const windowFocused = useWindowFocus();
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { withPageAuth } from "utils/auth";
|
|
||||||
import checkAndCopyConfig, { getSettings } from "utils/config/config";
|
import checkAndCopyConfig, { getSettings } from "utils/config/config";
|
||||||
import themes from "utils/styles/themes";
|
import themes from "utils/styles/themes";
|
||||||
|
|
||||||
async function getProps({ res }) {
|
export async function getServerSideProps({ res }) {
|
||||||
checkAndCopyConfig("settings.yaml");
|
checkAndCopyConfig("settings.yaml");
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -45,5 +44,3 @@ async function getProps({ res }) {
|
|||||||
export default function Webmanifest() {
|
export default function Webmanifest() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withPageAuth(getProps);
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { getToken } from "next-auth/jwt";
|
|
||||||
|
|
||||||
import { isAuthEnabled } from "utils/env";
|
|
||||||
|
|
||||||
export function isAllowedHost(host) {
|
|
||||||
if (process.env.HOMEPAGE_ALLOWED_HOSTS === "*") return true;
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
const allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`].concat(
|
|
||||||
process.env.HOMEPAGE_ALLOWED_HOSTS ? process.env.HOMEPAGE_ALLOWED_HOSTS.split(",") : [],
|
|
||||||
);
|
|
||||||
if (host && allowedHosts.includes(host)) return true;
|
|
||||||
console.error(
|
|
||||||
`Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withAllowedHost(handler) {
|
|
||||||
return async function hostCheckedHandler(req, res) {
|
|
||||||
if (!isAllowedHost(req.headers?.host)) {
|
|
||||||
return res.status(400).json({ error: "Host validation failed. See logs for more details." });
|
|
||||||
}
|
|
||||||
return handler(req, res);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match NextAuth, which picks the cookie name from the parsed URL protocol
|
|
||||||
export function secureCookie() {
|
|
||||||
try {
|
|
||||||
return new URL(process.env.NEXTAUTH_URL || process.env.HOMEPAGE_EXTERNAL_URL).protocol === "https:";
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authorized(req, res) {
|
|
||||||
if (!isAuthEnabled()) return true;
|
|
||||||
|
|
||||||
res.setHeader("Cache-Control", "private, no-store");
|
|
||||||
return Boolean(
|
|
||||||
await getToken({
|
|
||||||
req,
|
|
||||||
secret: process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET,
|
|
||||||
secureCookie: secureCookie(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withAuth(handler, { public: isPublic } = {}) {
|
|
||||||
return withAllowedHost(async function authenticatedHandler(req, res) {
|
|
||||||
if (!isPublic?.(req) && !(await authorized(req, res))) {
|
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
|
||||||
}
|
|
||||||
return handler(req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withPageAuth(getProps) {
|
|
||||||
return async function authenticatedPage(context) {
|
|
||||||
if (!isAllowedHost(context.req?.headers?.host)) {
|
|
||||||
return { notFound: true };
|
|
||||||
}
|
|
||||||
if (!(await authorized(context.req, context.res))) {
|
|
||||||
// resolvedUrl is a path supplied by Next.js, never an external destination.
|
|
||||||
const callbackUrl = context.resolvedUrl || "/";
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return getProps(context);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import { encode } from "next-auth/jwt";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import createMockRes from "test-utils/create-mock-res";
|
|
||||||
import { isAllowedHost, secureCookie, withAllowedHost, withAuth, withPageAuth } from "utils/auth";
|
|
||||||
|
|
||||||
describe("handler authentication", () => {
|
|
||||||
const secret = "test-session-secret-at-least-32-characters";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.stubEnv("HOMEPAGE_AUTH_ENABLED", "true");
|
|
||||||
vi.stubEnv("HOMEPAGE_AUTH_SECRET", secret);
|
|
||||||
vi.stubEnv("HOMEPAGE_EXTERNAL_URL", "http://localhost:3000");
|
|
||||||
vi.stubEnv("NEXTAUTH_SECRET", "");
|
|
||||||
vi.stubEnv("NEXTAUTH_URL", "");
|
|
||||||
});
|
|
||||||
afterEach(() => vi.unstubAllEnvs());
|
|
||||||
|
|
||||||
const request = (cookies = {}) => ({ headers: { "x-prerender-revalidate": "public-image-key" }, cookies });
|
|
||||||
|
|
||||||
it("rejects anonymous requests before the API handler runs, independently of middleware", async () => {
|
|
||||||
const handler = vi.fn();
|
|
||||||
const res = createMockRes();
|
|
||||||
await withAuth(handler)(request(), res);
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
expect(res.body).toEqual({ error: "Unauthorized" });
|
|
||||||
expect(res.headers["Cache-Control"]).toBe("private, no-store");
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each(["HOMEPAGE_EXTERNAL_URL", "NEXTAUTH_URL"])("session cookies with %s", (variable) => {
|
|
||||||
it.each([
|
|
||||||
["http://homepage.example", false],
|
|
||||||
["https://homepage.example", true],
|
|
||||||
["HTTP://homepage.example", false],
|
|
||||||
["HTTPS://homepage.example", true],
|
|
||||||
[" https://homepage.example ", true],
|
|
||||||
[" http://homepage.example ", false],
|
|
||||||
])("accepts a real session for %s", async (url, secure) => {
|
|
||||||
vi.stubEnv(variable, url);
|
|
||||||
const cookie = secure ? "__Secure-next-auth.session-token" : "next-auth.session-token";
|
|
||||||
const token = await encode({ token: { sub: "homepage" }, secret });
|
|
||||||
const req = request({ [cookie]: token });
|
|
||||||
const res = createMockRes();
|
|
||||||
const handler = vi.fn(() => res.send("private data"));
|
|
||||||
await withAuth(handler)(req, res);
|
|
||||||
expect(handler).toHaveBeenCalledWith(req, res);
|
|
||||||
expect(res.body).toBe("private data");
|
|
||||||
expect(res.headers["Cache-Control"]).toBe("private, no-store");
|
|
||||||
|
|
||||||
const getProps = vi.fn(() => ({ props: { title: "private page" } }));
|
|
||||||
expect(await withPageAuth(getProps)({ req, res })).toEqual({ props: { title: "private page" } });
|
|
||||||
expect(getProps).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(["expired", "wrong secret", "missing secret", "malformed"])("rejects %s sessions", async (kind) => {
|
|
||||||
const token =
|
|
||||||
kind === "malformed"
|
|
||||||
? "broken-token"
|
|
||||||
: await encode({
|
|
||||||
token: { sub: "homepage" },
|
|
||||||
secret: kind === "wrong secret" ? "different-secret" : secret,
|
|
||||||
maxAge: kind === "expired" ? -60 : 3600,
|
|
||||||
});
|
|
||||||
if (kind === "missing secret") vi.stubEnv("HOMEPAGE_AUTH_SECRET", "");
|
|
||||||
const handler = vi.fn();
|
|
||||||
const res = createMockRes();
|
|
||||||
await withAuth(handler)(request({ "next-auth.session-token": token }), res);
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows auth-disabled installations and explicit public resources", async () => {
|
|
||||||
const handler = vi.fn();
|
|
||||||
await withAuth(handler, { public: () => true })(request(), createMockRes());
|
|
||||||
vi.stubEnv("HOMEPAGE_AUTH_ENABLED", "false");
|
|
||||||
await withAuth(handler)(request(), createMockRes());
|
|
||||||
expect(handler).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("redirects a page before reading or returning its private props", async () => {
|
|
||||||
const getProps = vi.fn();
|
|
||||||
const res = createMockRes();
|
|
||||||
const result = await withPageAuth(getProps)({ req: request(), res, resolvedUrl: "/?tab=private" });
|
|
||||||
expect(result.redirect).toEqual({ destination: "/auth/signin?callbackUrl=%2F%3Ftab%3Dprivate", permanent: false });
|
|
||||||
expect(res.headers["Cache-Control"]).toBe("private, no-store");
|
|
||||||
expect(getProps).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("handler host validation", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.stubEnv("HOMEPAGE_ALLOWED_HOSTS", "");
|
|
||||||
vi.stubEnv("PORT", "3000");
|
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllEnvs();
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["localhost:3000", true],
|
|
||||||
["127.0.0.1:3000", true],
|
|
||||||
["[::1]:3000", true],
|
|
||||||
["evil.example", false],
|
|
||||||
[undefined, false],
|
|
||||||
])("checks %s against the default hosts", (host, allowed) => {
|
|
||||||
expect(isAllowedHost(host)).toBe(allowed);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts configured hosts and the wildcard", () => {
|
|
||||||
vi.stubEnv("HOMEPAGE_ALLOWED_HOSTS", "homepage.example,other:3000");
|
|
||||||
expect(isAllowedHost("homepage.example")).toBe(true);
|
|
||||||
expect(isAllowedHost("evil.example")).toBe(false);
|
|
||||||
vi.stubEnv("HOMEPAGE_ALLOWED_HOSTS", "*");
|
|
||||||
expect(isAllowedHost("evil.example")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects disallowed hosts before public or authenticated handlers run", async () => {
|
|
||||||
const handler = vi.fn();
|
|
||||||
const req = { headers: { host: "evil.example", "x-prerender-revalidate": "public-image-key" }, cookies: {} };
|
|
||||||
for (const wrapped of [withAllowedHost(handler), withAuth(handler, { public: () => true })]) {
|
|
||||||
const res = createMockRes();
|
|
||||||
await wrapped(req, res);
|
|
||||||
expect(res.statusCode).toBe(400);
|
|
||||||
}
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides pages from disallowed hosts before reading their props", async () => {
|
|
||||||
const getProps = vi.fn();
|
|
||||||
const result = await withPageAuth(getProps)({
|
|
||||||
req: { headers: { host: "evil.example" }, cookies: {} },
|
|
||||||
res: createMockRes(),
|
|
||||||
resolvedUrl: "/",
|
|
||||||
});
|
|
||||||
expect(result).toEqual({ notFound: true });
|
|
||||||
expect(getProps).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("secureCookie", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.stubEnv("NEXTAUTH_URL", "");
|
|
||||||
vi.stubEnv("HOMEPAGE_EXTERNAL_URL", "");
|
|
||||||
});
|
|
||||||
afterEach(() => vi.unstubAllEnvs());
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["https://homepage.example", true],
|
|
||||||
["HTTPS://homepage.example", true],
|
|
||||||
[" https://homepage.example ", true],
|
|
||||||
["http://homepage.example", false],
|
|
||||||
["homepage.example", false],
|
|
||||||
["not a url", false],
|
|
||||||
["", false],
|
|
||||||
])("treats %j as secure: %s", (url, secure) => {
|
|
||||||
vi.stubEnv("HOMEPAGE_EXTERNAL_URL", url);
|
|
||||||
expect(secureCookie()).toBe(secure);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prefers NEXTAUTH_URL, as NextAuth does", () => {
|
|
||||||
vi.stubEnv("NEXTAUTH_URL", "https://homepage.example");
|
|
||||||
vi.stubEnv("HOMEPAGE_EXTERNAL_URL", "http://homepage.example");
|
|
||||||
expect(secureCookie()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects instead of erroring when the URL is invalid", async () => {
|
|
||||||
vi.stubEnv("HOMEPAGE_AUTH_ENABLED", "true");
|
|
||||||
vi.stubEnv("HOMEPAGE_AUTH_SECRET", "test-session-secret-at-least-32-characters");
|
|
||||||
vi.stubEnv("HOMEPAGE_EXTERNAL_URL", "homepage.example");
|
|
||||||
const handler = vi.fn();
|
|
||||||
const res = createMockRes();
|
|
||||||
await withAuth(handler)({ headers: {}, cookies: {} }, res);
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -69,11 +69,11 @@ export function substituteEnvironmentVars(str) {
|
|||||||
const cachedVars = getCachedEnvironmentVars();
|
const cachedVars = getCachedEnvironmentVars();
|
||||||
cachedVars.forEach(([key, value]) => {
|
cachedVars.forEach(([key, value]) => {
|
||||||
if (key.startsWith(homepageVarPrefix)) {
|
if (key.startsWith(homepageVarPrefix)) {
|
||||||
result = result.replaceAll(`{{${key}}}`, () => value);
|
result = result.replaceAll(`{{${key}}}`, value);
|
||||||
} else if (key.startsWith(homepageFilePrefix)) {
|
} else if (key.startsWith(homepageFilePrefix)) {
|
||||||
const filename = value;
|
const filename = value;
|
||||||
const fileContents = readFileSync(filename, "utf8");
|
const fileContents = readFileSync(filename, "utf8");
|
||||||
result = result.replaceAll(`{{${key}}}`, () => fileContents);
|
result = result.replaceAll(`{{${key}}}`, fileContents);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mkdtempSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@@ -37,39 +37,6 @@ describe("utils/config/config", () => {
|
|||||||
expect(mod.substituteEnvironmentVars("token={{HOMEPAGE_FILE_SECRET}}")).toBe("token=secret");
|
expect(mod.substituteEnvironmentVars("token={{HOMEPAGE_FILE_SECRET}}")).toBe("token=secret");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["pa$$word", "pa$&word", "pa$`word", "pa$'word", "pa$1word"])(
|
|
||||||
"substituteEnvironmentVars preserves literal environment value %j in every occurrence",
|
|
||||||
async (secret) => {
|
|
||||||
process.env.HOMEPAGE_VAR_SECRET = secret;
|
|
||||||
|
|
||||||
const mod = await import("./config");
|
|
||||||
expect(
|
|
||||||
mod.substituteEnvironmentVars("before {{HOMEPAGE_VAR_SECRET}} between {{HOMEPAGE_VAR_SECRET}} after"),
|
|
||||||
).toBe(`before ${secret} between ${secret} after`);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.each(["pa$$word", "pa$&word", "pa$`word", "pa$'word", "pa$1word"])(
|
|
||||||
"substituteEnvironmentVars preserves literal secret file contents %j in every occurrence",
|
|
||||||
async (secret) => {
|
|
||||||
const dir = mkdtempSync(path.join(tmpdir(), "homepage-config-test-"));
|
|
||||||
const secretPath = path.join(dir, "secret.txt");
|
|
||||||
writeFileSync(secretPath, secret, "utf8");
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.env.HOMEPAGE_FILE_SECRET = secretPath;
|
|
||||||
|
|
||||||
const mod = await import("./config");
|
|
||||||
expect(
|
|
||||||
mod.substituteEnvironmentVars("before {{HOMEPAGE_FILE_SECRET}} between {{HOMEPAGE_FILE_SECRET}} after"),
|
|
||||||
).toBe(`before ${secret} between ${secret} after`);
|
|
||||||
} finally {
|
|
||||||
unlinkSync(secretPath);
|
|
||||||
rmdirSync(dir);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("getSettings reads from HOMEPAGE_CONFIG_DIR and converts layout list to an object", async () => {
|
it("getSettings reads from HOMEPAGE_CONFIG_DIR and converts layout list to an object", async () => {
|
||||||
const dir = mkdtempSync(path.join(tmpdir(), "homepage-settings-test-"));
|
const dir = mkdtempSync(path.join(tmpdir(), "homepage-settings-test-"));
|
||||||
process.env.HOMEPAGE_CONFIG_DIR = dir;
|
process.env.HOMEPAGE_CONFIG_DIR = dir;
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import cache from "memory-cache";
|
|
||||||
import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
|
|
||||||
|
|
||||||
import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
|
|
||||||
import { getSettings } from "utils/config/config";
|
|
||||||
import createLogger from "utils/logger";
|
|
||||||
|
|
||||||
const cacheKey = "homepageDashboardData";
|
|
||||||
|
|
||||||
export function invalidateDashboardData() {
|
|
||||||
cache.del(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadDashboardData() {
|
|
||||||
const { providers, ...settings } = getSettings();
|
|
||||||
const language = settings.language?.toLowerCase() === "zh-cn" ? "zh-Hans" : settings.language || "en";
|
|
||||||
const [services, bookmarks, widgets, translations] = await Promise.all([
|
|
||||||
servicesResponse(),
|
|
||||||
bookmarksResponse(),
|
|
||||||
widgetsResponse(),
|
|
||||||
serverSideTranslations(language),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
initialSettings: settings,
|
|
||||||
fallback: {
|
|
||||||
"/api/services": services,
|
|
||||||
"/api/bookmarks": bookmarks,
|
|
||||||
"/api/widgets": widgets,
|
|
||||||
"/api/hash": false,
|
|
||||||
},
|
|
||||||
...translations,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDashboardData() {
|
|
||||||
let pending = cache.get(cacheKey);
|
|
||||||
if (!pending) {
|
|
||||||
pending = loadDashboardData().catch((error) => {
|
|
||||||
if (cache.get(cacheKey) === pending) invalidateDashboardData();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
cache.put(cacheKey, pending);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await pending;
|
|
||||||
} catch (error) {
|
|
||||||
createLogger("index").error(error);
|
|
||||||
return {
|
|
||||||
initialSettings: {},
|
|
||||||
fallback: { "/api/services": [], "/api/bookmarks": [], "/api/widgets": [], "/api/hash": false },
|
|
||||||
...(await serverSideTranslations("en")),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const { services, bookmarks, widgets, translations, settings, logger } = vi.hoisted(() => ({
|
|
||||||
services: vi.fn(),
|
|
||||||
bookmarks: vi.fn(),
|
|
||||||
widgets: vi.fn(),
|
|
||||||
translations: vi.fn(),
|
|
||||||
settings: vi.fn(),
|
|
||||||
logger: { error: vi.fn() },
|
|
||||||
}));
|
|
||||||
vi.mock("utils/config/api-response", () => ({
|
|
||||||
servicesResponse: services,
|
|
||||||
bookmarksResponse: bookmarks,
|
|
||||||
widgetsResponse: widgets,
|
|
||||||
}));
|
|
||||||
vi.mock("utils/config/config", () => ({ getSettings: settings }));
|
|
||||||
vi.mock("next-i18next/pages/serverSideTranslations", () => ({ serverSideTranslations: translations }));
|
|
||||||
vi.mock("utils/logger", () => ({ default: () => logger }));
|
|
||||||
|
|
||||||
import { getDashboardData, invalidateDashboardData } from "utils/config/dashboard";
|
|
||||||
|
|
||||||
describe("dashboard data snapshot", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
invalidateDashboardData();
|
|
||||||
services.mockResolvedValue([{ name: "private service" }]);
|
|
||||||
bookmarks.mockResolvedValue([]);
|
|
||||||
widgets.mockResolvedValue([]);
|
|
||||||
translations.mockResolvedValue({ _nextI18Next: { initialLocale: "en" } });
|
|
||||||
settings.mockReturnValue({ providers: { privateKey: "secret" }, title: "Dashboard" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("coalesces concurrent discovery and reuses the snapshot for later page loads", async () => {
|
|
||||||
const results = await Promise.all(Array.from({ length: 10 }, () => getDashboardData()));
|
|
||||||
const later = await getDashboardData();
|
|
||||||
for (const loader of [services, bookmarks, widgets, translations, settings]) expect(loader).toHaveBeenCalledOnce();
|
|
||||||
expect(results.every((result) => result === later)).toBe(true);
|
|
||||||
expect(later.initialSettings).toEqual({ title: "Dashboard" });
|
|
||||||
expect(later.fallback["/api/services"]).toEqual([{ name: "private service" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("refreshes configuration and discovery after invalidation", async () => {
|
|
||||||
await getDashboardData();
|
|
||||||
invalidateDashboardData();
|
|
||||||
settings.mockReturnValue({ title: "Updated", language: "zh-CN" });
|
|
||||||
const result = await getDashboardData();
|
|
||||||
expect(services).toHaveBeenCalledTimes(2);
|
|
||||||
expect(result.initialSettings.title).toBe("Updated");
|
|
||||||
expect(translations).toHaveBeenLastCalledWith("zh-Hans");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retries after a failed load instead of caching an empty dashboard", async () => {
|
|
||||||
services.mockRejectedValueOnce(new Error("discovery failed"));
|
|
||||||
expect((await getDashboardData()).fallback["/api/services"]).toEqual([]);
|
|
||||||
expect((await getDashboardData()).fallback["/api/services"]).toEqual([{ name: "private service" }]);
|
|
||||||
expect(services).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not let a stale failed load evict a newer snapshot", async () => {
|
|
||||||
let reject;
|
|
||||||
services.mockImplementationOnce(
|
|
||||||
() =>
|
|
||||||
new Promise((_resolve, fail) => {
|
|
||||||
reject = fail;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const stale = getDashboardData();
|
|
||||||
invalidateDashboardData();
|
|
||||||
const current = await getDashboardData();
|
|
||||||
reject(new Error("old discovery failed"));
|
|
||||||
await stale;
|
|
||||||
expect(await getDashboardData()).toBe(current);
|
|
||||||
expect(services).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -55,7 +55,6 @@ export default async function credentialedProxyHandler(req, res, map) {
|
|||||||
"authentik",
|
"authentik",
|
||||||
"cloudflared",
|
"cloudflared",
|
||||||
"ghostfolio",
|
"ghostfolio",
|
||||||
"gitea",
|
|
||||||
"headscale",
|
"headscale",
|
||||||
"hoarder",
|
"hoarder",
|
||||||
"karakeep",
|
"karakeep",
|
||||||
@@ -144,7 +143,7 @@ export default async function credentialedProxyHandler(req, res, map) {
|
|||||||
headers["X-API-Key"] = `${widget.key}`;
|
headers["X-API-Key"] = `${widget.key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [status, contentType, data, responseHeaders] = await httpProxy(url, {
|
const [status, contentType, data] = await httpProxy(url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -179,7 +178,7 @@ export default async function credentialedProxyHandler(req, res, map) {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
|
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
|
||||||
}
|
}
|
||||||
if (map) resultData = map(resultData, responseHeaders);
|
if (map) resultData = map(resultData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType) res.setHeader("Content-Type", contentType);
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ vi.mock("widgets/widgets", () => ({
|
|||||||
autobrr: { api: "{url}/{endpoint}" },
|
autobrr: { api: "{url}/{endpoint}" },
|
||||||
jellystat: { api: "{url}/{endpoint}" },
|
jellystat: { api: "{url}/{endpoint}" },
|
||||||
trilium: { api: "{url}/{endpoint}" },
|
trilium: { api: "{url}/{endpoint}" },
|
||||||
gitea: { api: "{url}/api/v1/{endpoint}" },
|
|
||||||
gitlab: { api: "{url}/{endpoint}" },
|
gitlab: { api: "{url}/{endpoint}" },
|
||||||
azuredevops: { api: "{url}/{endpoint}" },
|
azuredevops: { api: "{url}/{endpoint}" },
|
||||||
glances: { api: "{url}/{endpoint}" },
|
glances: { api: "{url}/{endpoint}" },
|
||||||
@@ -136,33 +135,6 @@ describe("utils/proxy/handlers/credentialed", () => {
|
|||||||
expect(res.body).toEqual({ ok: true });
|
expect(res.body).toEqual({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Bearer auth for gitea widgets and keeps the key out of the URL", async () => {
|
|
||||||
getServiceWidget.mockResolvedValue({ type: "gitea", url: "http://example", key: "token" });
|
|
||||||
httpProxy.mockResolvedValue([200, "application/json", []]);
|
|
||||||
|
|
||||||
const req = { method: "GET", query: { group: "g", service: "s", endpoint: "notifications?limit=1", index: 0 } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await credentialedProxyHandler(req, res);
|
|
||||||
|
|
||||||
const [url, params] = httpProxy.mock.calls[0];
|
|
||||||
expect(params.headers.Authorization).toBe("Bearer token");
|
|
||||||
expect(url.toString()).toBe("http://example/api/v1/notifications?limit=1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes response headers to the mapping function", async () => {
|
|
||||||
getServiceWidget.mockResolvedValue({ type: "gitea", url: "http://example", key: "token" });
|
|
||||||
httpProxy.mockResolvedValue([200, "application/json", [], { "x-total-count": "7" }]);
|
|
||||||
|
|
||||||
const req = { method: "GET", query: { group: "g", service: "s", endpoint: "notifications?limit=1", index: 0 } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await credentialedProxyHandler(req, res, (data, headers) => ({ count: Number(headers["x-total-count"]) }));
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ count: 7 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses NC-Token auth for nextcloud widgets when key is provided", async () => {
|
it("uses NC-Token auth for nextcloud widgets when key is provided", async () => {
|
||||||
getServiceWidget.mockResolvedValue({ type: "nextcloud", url: "http://example", key: "nc-token" });
|
getServiceWidget.mockResolvedValue({ type: "nextcloud", url: "http://example", key: "nc-token" });
|
||||||
httpProxy.mockResolvedValue([200, "application/json", { ok: true }]);
|
httpProxy.mockResolvedValue([200, "application/json", { ok: true }]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
// Get total from header to avoid pagination limit
|
// Get total from header to avoid pagination limit
|
||||||
export function totalCount(data, headers) {
|
export function totalCount(data, headers) {
|
||||||
@@ -7,8 +7,8 @@ export function totalCount(data, headers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/api/v1/{endpoint}",
|
api: "{url}/api/v1/{endpoint}?access_token={key}",
|
||||||
proxyHandler: credentialedProxyHandler,
|
proxyHandler: genericProxyHandler,
|
||||||
|
|
||||||
mappings: {
|
mappings: {
|
||||||
notifications: {
|
notifications: {
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ afterEach(() => {
|
|||||||
if (typeof document !== "undefined") cleanup();
|
if (typeof document !== "undefined") cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
process.env.HOMEPAGE_ALLOWED_HOSTS = "*";
|
|
||||||
|
|
||||||
// Avoid NextAuth client-side fetches during unit tests.
|
// Avoid NextAuth client-side fetches during unit tests.
|
||||||
vi.mock("next-auth/react", () => ({
|
vi.mock("next-auth/react", () => ({
|
||||||
SessionProvider: ({ children }) => children ?? null,
|
SessionProvider: ({ children }) => children ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user