mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-11-04 01:00:20 +00:00
fix: prompt login when guest API returns unauthorized (#98)
When force_auth is on, the guest homepage still loads, but /api/v1/server
returns {"error":"ApiErrorUnauthorized"}. BackendErrorState treated that
the same as a down backend.
Match that error exactly and show a login title, description, and a
button to /dashboard. Other errors keep the old copy and the raw message.
zh-TW is unchanged: that file uses spaces, and biome would reformat the
whole translation.
This commit is contained in:
1 parent
d4fe4e5806
commit
2750d9858a
8 files changed
+89
-12
No files matched your search
@@ -9,6 +9,14 @@ import type {
|
||||
SettingResponse,
|
||||
} from "@/types/nezha-api";
|
||||
|
||||
export const API_ERROR_UNAUTHORIZED = "ApiErrorUnauthorized";
|
||||
|
||||
export function isUnauthorizedError(error: unknown): boolean {
|
||||
const message =
|
||||
error instanceof Error ? error.message : error == null ? "" : String(error);
|
||||
return message === API_ERROR_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
let lastestRefreshTokenAt = 0;
|
||||
|
||||
const getCookieValue = (name: string): string | undefined => {
|
||||
|
||||
@@ -115,7 +115,9 @@
|
||||
"pageNotFound": "Page not found",
|
||||
"backToHome": "Back to home",
|
||||
"backendUnavailableTitle": "Backend API unavailable",
|
||||
"backendUnavailableDescription": "The page is still available, but data cannot be loaded right now."
|
||||
"backendUnavailableDescription": "The page is still available, but data cannot be loaded right now.",
|
||||
"loginRequiredTitle": "Login required",
|
||||
"loginRequiredDescription": "This site requires a login to load monitoring data."
|
||||
},
|
||||
"tabSwitch": {
|
||||
"Detail": "Detail",
|
||||
|
||||
@@ -108,7 +108,9 @@
|
||||
"pageNotFound": "Halaman tidak ditemukan",
|
||||
"backToHome": "Kembali ke beranda",
|
||||
"backendUnavailableTitle": "API backend tidak tersedia",
|
||||
"backendUnavailableDescription": "Halaman masih tersedia, tetapi data tidak dapat dimuat saat ini."
|
||||
"backendUnavailableDescription": "Halaman masih tersedia, tetapi data tidak dapat dimuat saat ini.",
|
||||
"loginRequiredTitle": "Perlu masuk",
|
||||
"loginRequiredDescription": "Situs ini memerlukan login untuk memuat data pemantauan."
|
||||
},
|
||||
"tabSwitch": {
|
||||
"Detail": "Detail",
|
||||
|
||||
@@ -108,7 +108,9 @@
|
||||
"pageNotFound": "Página não encontrada",
|
||||
"backToHome": "Voltar ao inicio",
|
||||
"backendUnavailableTitle": "API Backend indisponível",
|
||||
"backendUnavailableDescription": "A página ainda está disponível, mas os dados não podem ser carregados agora."
|
||||
"backendUnavailableDescription": "A página ainda está disponível, mas os dados não podem ser carregados agora.",
|
||||
"loginRequiredTitle": "Login necessário",
|
||||
"loginRequiredDescription": "Este site exige login para carregar os dados de monitoramento."
|
||||
},
|
||||
"tabSwitch": {
|
||||
"Detail": "Detalhes",
|
||||
|
||||
@@ -109,7 +109,9 @@
|
||||
"pageNotFound": "页面不存在",
|
||||
"backToHome": "回到主页",
|
||||
"backendUnavailableTitle": "后端 API 无法访问",
|
||||
"backendUnavailableDescription": "页面仍会正常显示,但暂时无法加载监控数据。"
|
||||
"backendUnavailableDescription": "页面仍会正常显示,但暂时无法加载监控数据。",
|
||||
"loginRequiredTitle": "需要登录",
|
||||
"loginRequiredDescription": "当前站点已开启强制登录,登录后即可加载监控数据。"
|
||||
},
|
||||
"tabSwitch": {
|
||||
"Detail": "详情",
|
||||
|
||||
+27
-7
@@ -17,11 +17,16 @@ import ServerCard from "@/components/ServerCard";
|
||||
import ServerCardInline from "@/components/ServerCardInline";
|
||||
import ServerOverview from "@/components/ServerOverview";
|
||||
import { ServiceTracker } from "@/components/ServiceTracker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SORT_TYPES } from "@/context/sort-context";
|
||||
import { useSort } from "@/hooks/use-sort";
|
||||
import { useStatus } from "@/hooks/use-status";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { fetchServerGroup, fetchService } from "@/lib/nezha-api";
|
||||
import {
|
||||
fetchServerGroup,
|
||||
fetchService,
|
||||
isUnauthorizedError,
|
||||
} from "@/lib/nezha-api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { NezhaServer, ServerGroup } from "@/types/nezha-api";
|
||||
|
||||
@@ -53,21 +58,36 @@ const getErrorMessage = (error: unknown) => {
|
||||
|
||||
function BackendErrorState({ error }: { error: unknown }) {
|
||||
const { t } = useTranslation();
|
||||
const unauthorized = isUnauthorizedError(error);
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-96 flex-col items-center justify-center px-4 text-center">
|
||||
<div className="flex max-w-md flex-col items-center gap-2">
|
||||
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">
|
||||
{t("error.backendUnavailableTitle")}
|
||||
{t(
|
||||
unauthorized
|
||||
? "error.loginRequiredTitle"
|
||||
: "error.backendUnavailableTitle",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("error.backendUnavailableDescription")}
|
||||
{t(
|
||||
unauthorized
|
||||
? "error.loginRequiredDescription"
|
||||
: "error.backendUnavailableDescription",
|
||||
)}
|
||||
</p>
|
||||
{message && (
|
||||
<p className="mt-1 max-w-full break-words rounded-md bg-stone-100 px-2 py-1 text-xs text-stone-500 dark:bg-stone-800 dark:text-stone-400">
|
||||
{message}
|
||||
</p>
|
||||
{unauthorized ? (
|
||||
<Button asChild className="mt-2">
|
||||
<a href="/dashboard">{t("login")}</a>
|
||||
</Button>
|
||||
) : (
|
||||
message && (
|
||||
<p className="mt-1 max-w-full break-words rounded-md bg-stone-100 px-2 py-1 text-xs text-stone-500 dark:bg-stone-800 dark:text-stone-400">
|
||||
{message}
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
fetchServerMetrics,
|
||||
fetchService,
|
||||
fetchSetting,
|
||||
isUnauthorizedError,
|
||||
} from "@/lib/nezha-api";
|
||||
|
||||
const jsonResponse = (body: unknown, init?: ResponseInit) =>
|
||||
@@ -52,6 +53,12 @@ describe("nezha api fetchers", () => {
|
||||
await expect(fetchService()).rejects.toThrow("service unavailable");
|
||||
});
|
||||
|
||||
it("detects guest-auth errors from the dashboard API", () => {
|
||||
expect(isUnauthorizedError(new Error("ApiErrorUnauthorized"))).toBe(true);
|
||||
expect(isUnauthorizedError(new Error("settings failed"))).toBe(false);
|
||||
expect(isUnauthorizedError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("adds monitor and metrics query parameters when periods are provided", async () => {
|
||||
vi.mocked(fetch)
|
||||
.mockResolvedValueOnce(jsonResponse({ success: true, data: [] }))
|
||||
|
||||
@@ -16,7 +16,15 @@ const apiMocks = vi.hoisted(() => ({
|
||||
fetchService: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/nezha-api", () => apiMocks);
|
||||
vi.mock("@/lib/nezha-api", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/nezha-api")>("@/lib/nezha-api");
|
||||
return {
|
||||
...actual,
|
||||
fetchServerGroup: apiMocks.fetchServerGroup,
|
||||
fetchService: apiMocks.fetchService,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/GlobalMap", () => ({
|
||||
default: ({ serverList }: { serverList: NezhaServer[] }) => (
|
||||
@@ -217,6 +225,32 @@ describe("Servers page", () => {
|
||||
expect(screen.getByText("group failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("prompts guests to log in when the API returns ApiErrorUnauthorized", async () => {
|
||||
apiMocks.fetchServerGroup.mockRejectedValue(
|
||||
new Error("ApiErrorUnauthorized"),
|
||||
);
|
||||
|
||||
renderServerPage({
|
||||
connected: false,
|
||||
lastData: null,
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText("error.loginRequiredTitle"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("error.loginRequiredDescription"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "login" })).toHaveAttribute(
|
||||
"href",
|
||||
"/dashboard",
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("error.backendUnavailableTitle"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("ApiErrorUnauthorized")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("summarizes online and offline servers from websocket data", async () => {
|
||||
const online = createServer({ id: 1, name: "alpha" });
|
||||
const offline = createServer({
|
||||
|
||||
Reference in new issue
Block a user