diff --git a/postcss.config.js b/postcss.config.js index d41ad63..521f73c 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, }, } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 6cd3d19..0b9d72c 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -70,7 +70,7 @@ function Calendar({ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", defaultClassNames.caption_label, ), - table: "w-full border-collapse", + month_grid: "w-full border-collapse", weekdays: cn("flex", defaultClassNames.weekdays), weekday: cn( "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", diff --git a/src/index.css b/src/index.css index f533e66..9e2c894 100644 --- a/src/index.css +++ b/src/index.css @@ -1,62 +1,95 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@plugin "tailwindcss-animate"; -@layer base { - :root { - --radius: 0.5rem; - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } +@custom-variant dark (&:is(.dark *)); - .dark { - --background: 0 0% 9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } +@theme inline { + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --text-xsm: 0.825rem; + --text-xsm--line-height: 1.25rem; +} + +:root { + --radius: 0.5rem; + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; +} + +.dark { + --background: 0 0% 9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } @layer base { diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 3607f18..bd39cbb 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -148,6 +148,8 @@ "Loading": "Laden", "Services": "Services", "DashboardOriginalHost": "Agent Verbindungsadresse [Domainname/IP:port]", + "DashboardHost": "Dashboard-Host für OAuth2-Callback [Domainname/IP:port]", + "DashboardHostHint": "Der öffentliche Host des Dashboards zum Erstellen der OAuth2-Callback-URL. Setzen Sie diesen Wert, wenn das Dashboard über eine andere Domain als die Agent-Verbindungsadresse erreichbar ist. Leer lassen, um dem Request-Host unverändert zu vertrauen.", "ReservedHosts": "Reservierte Hosts (kommagetrennt; öffentliche/Reverse-Proxy-Domains, die Mitglieder nicht als NAT-Domain nutzen dürfen)", "ReservedHostsHint": "Hinter einem Reverse-Proxy sieht das Dashboard seine eigene öffentliche Domain nicht und ist daher nicht automatisch geschützt. Tragen Sie hier jeden öffentlichen Eingangs-Hostnamen ein, sonst könnte ein Mitglied ihn als NAT-Domain registrieren und das Dashboard-Routing übernehmen.", "NavigateTo": "Navigieren", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4b17e3e..63aa114 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -141,6 +141,8 @@ "WAF": "Web Application Firewall", "SiteName": "Site Name", "DashboardOriginalHost": "Agent connecting address [domain name/IP:port]", + "DashboardHost": "Dashboard host for OAuth2 callback [domain name/IP:port]", + "DashboardHostHint": "The dashboard's public host used to build the OAuth2 callback URL. Set this when the dashboard is reached on a different domain than the agent connecting address. Leave empty to trust the request Host as-is.", "ReservedHosts": "Reserved hosts (comma-separated; public/reverse-proxy domains members cannot use as NAT domains)", "ReservedHostsHint": "Behind a reverse proxy the dashboard cannot see its own public domain, so it is not auto-protected. List every public entry hostname here, or a member could register it as a NAT domain and hijack dashboard routing.", "ConfigTLS": "Use TLS to connect Agent", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index c24a5b6..d39cd37 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -157,6 +157,8 @@ "RequestType": "Tipo de solicitud", "RequestBody": "Cuerpo de la solicitud", "DashboardOriginalHost": "Dirección de conexión del Agent [nombre de dominio/IP:puerto]", + "DashboardHost": "Host del panel para el callback de OAuth2 [nombre de dominio/IP:puerto]", + "DashboardHostHint": "Host público del panel usado para construir la URL de callback de OAuth2. Configúralo cuando se acceda al panel en un dominio distinto al de la dirección de conexión del Agent. Déjalo vacío para confiar en el Host de la petición tal cual.", "ReservedHosts": "Hosts reservados (separados por comas; dominios públicos/proxy inverso que los miembros no pueden usar como dominio NAT)", "ReservedHostsHint": "Detrás de un proxy inverso, el panel no ve su propio dominio público, por lo que no se protege automáticamente. Indique aquí todos los nombres de host de entrada públicos; de lo contrario, un miembro podría registrarlo como dominio NAT y secuestrar el enrutamiento del panel.", "ConfigTLS": "Usar TLS para conectar el Agent", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index f01cce0..24f67f8 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -138,6 +138,8 @@ "WAF": "Firewall Aplikasi Web", "SiteName": "Nama Situs", "DashboardOriginalHost": "Alamat koneksi Agen [nama domain/IP:port]", + "DashboardHost": "Host dashboard untuk callback OAuth2 [nama domain/IP:port]", + "DashboardHostHint": "Host publik dashboard yang dipakai untuk membangun URL callback OAuth2. Atur ini bila dashboard diakses pada domain yang berbeda dari alamat koneksi Agen. Kosongkan untuk mempercayai Host permintaan apa adanya.", "ReservedHosts": "Host khusus (dipisahkan koma; domain publik/reverse-proxy yang tidak boleh dipakai anggota sebagai domain NAT)", "ReservedHostsHint": "Di belakang reverse proxy, dasbor tidak melihat domain publiknya sendiri sehingga tidak terlindungi otomatis. Cantumkan setiap host entri publik di sini, atau anggota bisa mendaftarkannya sebagai domain NAT dan membajak perutean dasbor.", "ConfigTLS": "Gunakan TLS untuk menghubungkan Agen", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index f432306..4f5baff 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -141,6 +141,8 @@ "CustomCodes": "Codice personalizzato (stili e script)", "CustomCodesDashboard": "Codice personalizzato per dashboard", "DashboardOriginalHost": "Indirizzo di ancoraggio dell'agente [nome dominio/IP:porta]", + "DashboardHost": "Host della dashboard per il callback OAuth2 [nome dominio/IP:porta]", + "DashboardHostHint": "Host pubblico della dashboard usato per costruire l'URL di callback OAuth2. Impostalo quando la dashboard è raggiungibile su un dominio diverso dall'indirizzo di connessione dell'agente. Lascia vuoto per fidarti dell'Host della richiesta così com'è.", "ReservedHosts": "Host riservati (separati da virgola; domini pubblici/reverse-proxy che i membri non possono usare come dominio NAT)", "ReservedHostsHint": "Dietro un reverse proxy il pannello non vede il proprio dominio pubblico, quindi non è protetto automaticamente. Elenca qui ogni hostname di ingresso pubblico, altrimenti un membro potrebbe registrarlo come dominio NAT e dirottare il routing del pannello.", "ConfigTLS": "Usa TLS per connettere Agent", diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json index a1edca8..88f2ac2 100644 --- a/src/locales/ro/translation.json +++ b/src/locales/ro/translation.json @@ -140,6 +140,8 @@ "WAF": "Web Application Firewall", "SiteName": "Numele site-ului", "DashboardOriginalHost": "Adresa de conectare a agentului [numele domeniului/IP:port]", + "DashboardHost": "Host-ul panoului pentru callback-ul OAuth2 [numele domeniului/IP:port]", + "DashboardHostHint": "Host-ul public al panoului folosit pentru a construi URL-ul de callback OAuth2. Setați-l când panoul este accesat pe un domeniu diferit de adresa de conectare a agentului. Lăsați gol pentru a avea încredere în Host-ul cererii ca atare.", "ReservedHosts": "Host-uri reservate (separate prin virgulă; domeniile publice sau ale reverse proxy-urilor nu pot fi utilizate ca domenii NAT)", "ReservedHostsHint": "În spatele unui reverse proxy, dashboard-ul nu își poate vedea propriul domeniu public, prin urmare nu este protejat automat. Trebuie să menționați aici toate hostname-urile ale intrărilor publice; în caz contrar, un membru ar putea să le înregistreze ca domenii NAT și să deturneze rutarea către dashboard.", "ConfigTLS": "Folosește TLS pentru a conecta agentul", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 9430b18..7b89e81 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -42,6 +42,8 @@ "Credential": "Учетные данные", "CommunityThemeDescription": "Эта тема предоставлена сообществом, используйте на свой страх и риск", "DashboardOriginalHost": "Адрес подключения агента [домен/IP:порт]", + "DashboardHost": "Хост панели для OAuth2 callback [домен/IP:порт]", + "DashboardHostHint": "Публичный хост панели, используемый для формирования URL обратного вызова OAuth2. Укажите его, если панель доступна на домене, отличном от адреса подключения агента. Оставьте пустым, чтобы доверять Host из запроса как есть.", "ReservedHosts": "Зарезервированные хосты (через запятую; публичные/реверс-прокси домены, которые участники не могут использовать как NAT-домен)", "ReservedHostsHint": "За реверс-прокси панель не видит собственный публичный домен и не защищается автоматически. Укажите здесь все публичные входные хосты, иначе участник сможет зарегистрировать такой домен как NAT-домен и перехватить маршрутизацию панели.", "Error": "Ошибка", diff --git a/src/locales/ta/translation.json b/src/locales/ta/translation.json index 0f04f41..0399dec 100644 --- a/src/locales/ta/translation.json +++ b/src/locales/ta/translation.json @@ -134,6 +134,8 @@ "WAF": "வலை பயன்பாடு ஃபயர்வால்", "SiteName": "தளத்தின் பெயர்", "DashboardOriginalHost": "முகவரியை இணைக்கும் முகவரி [டொமைன் பெயர்/ஐபி: போர்ட்]", + "DashboardHost": "OAuth2 கால்பேக்கிற்கான டாஷ்போர்டு ஹோஸ்ட் [டொமைன் பெயர்/ஐபி: போர்ட்]", + "DashboardHostHint": "OAuth2 கால்பேக் URL-ஐ உருவாக்கப் பயன்படும் டாஷ்போர்டின் பொது ஹோஸ்ட். ஏஜெண்ட் இணைப்பு முகவரியிலிருந்து வேறுபட்ட டொமைனில் டாஷ்போர்டு அணுகப்பட்டால் இதை அமைக்கவும். கோரிக்கையின் Host-ஐ அப்படியே நம்ப காலியாக விடவும்.", "ReservedHosts": "ஒதுக்கப்பட்ட ஹோஸ்ட்கள் (கமாவால் பிரிக்கப்பட்டது; உறுப்பினர்கள் NAT டொமைனாகப் பயன்படுத்த முடியாத பொது/ரிவர்ஸ்-ப்ராக்ஸி டொமைன்கள்)", "ReservedHostsHint": "ரிவர்ஸ்-ப்ராக்ஸிக்குப் பின்னால் டாஷ்போர்டு அதன் சொந்த பொது டொமைனைப் பார்க்க முடியாது, எனவே அது தானாகப் பாதுகாக்கப்படாது. அனைத்து பொது நுழைவு ஹோஸ்ட்களையும் இங்கே பட்டியலிடுங்கள்; இல்லையெனில் ஒரு உறுப்பினர் அதை NAT டொமைனாகப் பதிவுசெய்து டாஷ்போர்டு வழித்தடத்தைக் கடத்தலாம்.", "ConfigTLS": "முகவரை இணைக்க TLS ஐப் பயன்படுத்தவும்", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 1103350..4fbec49 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -154,6 +154,8 @@ "CustomCodes": "自定义代码(样式和脚本)", "CustomCodesDashboard": "仪表板的自定义代码", "DashboardOriginalHost": "Agent对接地址【域名/IP:端口】", + "DashboardHost": "面板 OAuth2 回调域名【域名/IP:端口】", + "DashboardHostHint": "用于生成 OAuth2 回调地址的面板对外域名。当面板访问域名与 Agent 对接地址不同时设置此项。留空则信任请求 Host,不做强制重写。", "ReservedHosts": "保留 Host(逗号分隔;反代/公网域名,成员不可注册为 NAT 域名)", "ReservedHostsHint": "反代部署下面板看不到自身的对外域名,无法自动保护。请在此列出所有公网入口域名,否则成员可将其注册为 NAT 域名抢占面板路由。", "ConfigTLS": "Agent 使用 TLS 连接", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index cce38f7..5f1537b 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -146,6 +146,8 @@ "CustomCodes": "自訂程式碼(樣式和腳本)", "CustomCodesDashboard": "儀表板的自訂程式碼", "DashboardOriginalHost": "Agent對接位址【網域名稱/IP:連接埠】", + "DashboardHost": "面板 OAuth2 回呼網域【網域名稱/IP:連接埠】", + "DashboardHostHint": "用於產生 OAuth2 回呼位址的面板對外網域。當面板存取網域與 Agent 對接位址不同時設定此項。留空則信任請求 Host,不做強制重寫。", "ReservedHosts": "保留 Host(逗號分隔;反代/公網網域,成員不可註冊為 NAT 網域)", "ReservedHostsHint": "反代部署下面板看不到自身的對外網域,無法自動保護。請在此列出所有公網入口網域,否則成員可將其註冊為 NAT 網域搶佔面板路由。", "ConfigTLS": "Agent 使用 TLS 連線", diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index 3d09342..829f594 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -45,6 +45,7 @@ const settingFormSchema = z.object({ language: z.string().min(2), user_template: z.string().min(1), install_host: asOptionalField(z.string()), + dashboard_host: asOptionalField(z.string()), reserved_hosts: asOptionalField(z.string()), custom_code: asOptionalField(z.string()), custom_code_dashboard: asOptionalField(z.string()), @@ -298,6 +299,20 @@ export default function SettingsPage() { )} /> + ( + + {t("DashboardHost")} + + + + {t("DashboardHostHint")} + + + )} + /> { + globalThis.fetch = vi.fn(async () => { return new Response(JSON.stringify({ success, error: success ? "" : "boom", data: payload }), { status: ok ? 200 : 500, headers: { "Content-Type": "application/json" }, @@ -19,13 +19,13 @@ beforeEach(() => { afterEach(() => { vi.useRealTimers() - global.fetch = realFetch + globalThis.fetch = realFetch vi.restoreAllMocks() }) test("listApiTokens GETs /api/v1/api-tokens and returns parsed array", async () => { const calls: Array<{ url: string; method: string }> = [] - global.fetch = vi.fn(async (input: any, init?: any) => { + globalThis.fetch = vi.fn(async (input: any, init?: any) => { calls.push({ url: String(input), method: String(init?.method ?? "GET") }) return new Response( JSON.stringify({ @@ -54,7 +54,7 @@ test("listApiTokens GETs /api/v1/api-tokens and returns parsed array", async () test("createApiToken POSTs /api/v1/api-tokens and serializes scopes / server_ids / expires_in_days", async () => { let captured: { url: string; method: string; body: any } | null = null - global.fetch = vi.fn(async (input: any, init?: any) => { + globalThis.fetch = vi.fn(async (input: any, init?: any) => { captured = { url: String(input), method: String(init?.method ?? ""), @@ -103,7 +103,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot // Backend APIToken.Scopes() returns nil for ScopesCSV=="" which JSON-encodes // as null; migrated/legacy/hand-edited rows hit this. Without normalization // the list page does tok.scopes.map(...) on null and the whole page crashes. - global.fetch = vi.fn(async () => { + globalThis.fetch = vi.fn(async () => { return new Response( JSON.stringify({ success: true, @@ -122,7 +122,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot test("deleteApiToken DELETEs /api/v1/api-tokens/:id", async () => { const calls: Array<{ url: string; method: string }> = [] - global.fetch = vi.fn(async (input: any, init?: any) => { + globalThis.fetch = vi.fn(async (input: any, init?: any) => { calls.push({ url: String(input), method: String(init?.method ?? "GET") }) return new Response(JSON.stringify({ success: true, data: null }), { status: 200, diff --git a/src/test/fetcher-csrf.test.ts b/src/test/fetcher-csrf.test.ts index b48055e..20e6e0e 100644 --- a/src/test/fetcher-csrf.test.ts +++ b/src/test/fetcher-csrf.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest" import { FetcherMethod, fetcher } from "../api/api" -const realFetch = global.fetch +const realFetch = globalThis.fetch function setCookie(value: string) { Object.defineProperty(document, "cookie", { value, configurable: true }) @@ -13,7 +13,7 @@ beforeEach(() => { }) afterEach(() => { - global.fetch = realFetch + globalThis.fetch = realFetch vi.restoreAllMocks() }) @@ -40,7 +40,7 @@ function headerOf(init: RequestInit | undefined, name: string): string | null { test("POST sends X-CSRF-Token mirrored from nz-csrf cookie", async () => { setCookie("nz-csrf=abc123; other=1") const seen: { init?: RequestInit }[] = [] - global.fetch = vi.fn(async (_input: any, init?: RequestInit) => { + globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => { seen.push({ init }) return jsonOk() }) as unknown as typeof fetch @@ -53,7 +53,7 @@ test("POST sends X-CSRF-Token mirrored from nz-csrf cookie", async () => { test("DELETE sends X-CSRF-Token mirrored from nz-csrf cookie", async () => { setCookie("nz-csrf=del-token") const seen: { init?: RequestInit }[] = [] - global.fetch = vi.fn(async (_input: any, init?: RequestInit) => { + globalThis.fetch = vi.fn(async (_input: any, init?: RequestInit) => { seen.push({ init }) return jsonOk() }) as unknown as typeof fetch @@ -68,7 +68,7 @@ test("auto refresh-token uses POST (backend route is POST)", async () => { const { FetcherMethod: M, fetcher: f } = await import("../api/api") setCookie("nz-jwt=sess; nz-csrf=c") const seen: { url: string; init?: RequestInit }[] = [] - global.fetch = vi.fn(async (input: any, init?: RequestInit) => { + globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => { seen.push({ url: String(input), init }) return jsonOk() }) as unknown as typeof fetch @@ -83,13 +83,13 @@ test("auto refresh-token uses POST (backend route is POST)", async () => { // Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not // blow up on response.json() of an empty body and must resolve successfully. test("DELETE tolerates an empty 204 response body", async () => { - global.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch + globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined() }) test("empty 200 body does not throw", async () => { - global.fetch = vi.fn( + globalThis.fetch = vi.fn( async () => new Response("", { status: 200 }), ) as unknown as typeof fetch diff --git a/src/test/fetcher-delete.test.ts b/src/test/fetcher-delete.test.ts index 96ec46e..7ce48bb 100644 --- a/src/test/fetcher-delete.test.ts +++ b/src/test/fetcher-delete.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest" import { FetcherMethod, fetcher } from "../api/api" -const realFetch = global.fetch +const realFetch = globalThis.fetch beforeEach(() => { // Avoid the auto refresh-token branch interfering with assertions. @@ -10,7 +10,7 @@ beforeEach(() => { }) afterEach(() => { - global.fetch = realFetch + globalThis.fetch = realFetch vi.restoreAllMocks() }) @@ -19,7 +19,7 @@ afterEach(() => { // DELETE route. The wire-level method must match the requested FetcherMethod. test("fetcher uses HTTP DELETE for FetcherMethod.DELETE", async () => { const seen: { url: string; init?: RequestInit }[] = [] - global.fetch = vi.fn(async (input: any, init?: RequestInit) => { + globalThis.fetch = vi.fn(async (input: any, init?: RequestInit) => { seen.push({ url: String(input), init }) return new Response(JSON.stringify({ success: true, data: null }), { status: 200, diff --git a/src/test/fetcher-refresh-selfheal.test.ts b/src/test/fetcher-refresh-selfheal.test.ts index b92825e..e840335 100644 --- a/src/test/fetcher-refresh-selfheal.test.ts +++ b/src/test/fetcher-refresh-selfheal.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest" -const realFetch = global.fetch +const realFetch = globalThis.fetch function setCookie(value: string) { Object.defineProperty(document, "cookie", { value, configurable: true }) @@ -19,7 +19,7 @@ beforeEach(() => { }) afterEach(() => { - global.fetch = realFetch + globalThis.fetch = realFetch vi.restoreAllMocks() }) @@ -31,7 +31,7 @@ test("auto refresh is deferred while nz-csrf cookie is missing", async () => { const { FetcherMethod, fetcher } = await import("../api/api") setCookie("nz-jwt=session") // jwt present, but no nz-csrf yet const urls: string[] = [] - global.fetch = vi.fn(async (input: any) => { + globalThis.fetch = vi.fn(async (input: any) => { urls.push(String(input)) return jsonOk() }) as unknown as typeof fetch @@ -46,7 +46,7 @@ test("auto refresh is deferred while nz-csrf cookie is missing", async () => { test("auto refresh fires after nz-csrf cookie becomes available", async () => { const { FetcherMethod, fetcher } = await import("../api/api") const urls: string[] = [] - global.fetch = vi.fn(async (input: any) => { + globalThis.fetch = vi.fn(async (input: any) => { urls.push(String(input)) return jsonOk() }) as unknown as typeof fetch diff --git a/src/test/fm.test.tsx b/src/test/fm.test.tsx index 1f6fb32..4dabb7d 100644 --- a/src/test/fm.test.tsx +++ b/src/test/fm.test.tsx @@ -130,7 +130,7 @@ class MockWebSocket extends EventTarget implements WebSocket { onerror: ((this: WebSocket, ev: Event) => unknown) | null = null onmessage: ((this: WebSocket, ev: MessageEvent) => unknown) | null = null onopen: ((this: WebSocket, ev: Event) => unknown) | null = null - readyState = MockWebSocket.CONNECTING + readyState: 0 | 1 | 2 | 3 = MockWebSocket.CONNECTING sent: SentWebSocketData[] = [] constructor(url: string | URL) { diff --git a/src/test/setup.ts b/src/test/setup.ts index ed4ad93..2f33a70 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -21,6 +21,7 @@ class TestResizeObserver implements ResizeObserver { class TestIntersectionObserver implements IntersectionObserver { readonly root: Element | Document | null = null readonly rootMargin = "" + readonly scrollMargin = "" readonly thresholds: ReadonlyArray = [] observe(): void {} diff --git a/src/types/api.ts b/src/types/api.ts index 25020a2..88460e8 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -719,6 +719,7 @@ export interface ModelSetting { ignored_ip_notification: string ignored_ip_notification_server_ids: Record install_host: string + dashboard_host: string ip_change_notification_group_id: number /** 系统语言,默认 zh_CN */ language: string @@ -745,6 +746,7 @@ export interface ModelSettingForm { enable_mcp?: boolean ignored_ip_notification?: string install_host?: string + dashboard_host?: string reserved_hosts?: string /** IP变更提醒的通知组 */ ip_change_notification_group_id: number diff --git a/tsconfig.app.json b/tsconfig.app.json index bd905d3..9284bbc 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/tsconfig.json b/tsconfig.json index d82983e..77ed242 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ } ], "compilerOptions": { + "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "@/*": ["./src/*"]