diff --git a/.source/source.config.mjs b/.source/source.config.mjs index 215b34afa0..1b0644fe1e 100644 --- a/.source/source.config.mjs +++ b/.source/source.config.mjs @@ -11,9 +11,12 @@ var docs = defineDocs({ "./routing/**/*.md", "./security/**/*.md", "./compression/**/*.md", - "./ops/**/*.md", - ], - }, + "./ops/**/*.md" + ] + } }); var source_config_default = defineConfig(); -export { source_config_default as default, docs }; +export { + source_config_default as default, + docs +}; diff --git a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx index de5351852f..1a84b999ac 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Button, Input, Toggle } from "@/shared/components"; +import { Card, Button, Input, Toggle, Modal } from "@/shared/components"; import { AI_PROVIDERS } from "@/shared/constants/providers"; import IPFilterSection from "./IPFilterSection"; import SessionInfoCard from "./SessionInfoCard"; @@ -14,7 +14,15 @@ export default function SecurityTab() { const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); const [passStatus, setPassStatus] = useState({ type: "", message: "" }); const [passLoading, setPassLoading] = useState(false); + + const [requireLoginModalOpen, setRequireLoginModalOpen] = useState(false); + const [pendingRequireLoginVal, setPendingRequireLoginVal] = useState(null); + const [requireLoginPassword, setRequireLoginPassword] = useState(""); + const [requireLoginError, setRequireLoginError] = useState(""); + const [requireLoginLoading, setRequireLoginLoading] = useState(false); + const t = useTranslations("settings"); + const tc = useTranslations("common"); useEffect(() => { fetch("/api/settings") @@ -26,7 +34,15 @@ export default function SecurityTab() { .catch(() => setLoading(false)); }, []); - const updateRequireLogin = async (requireLogin) => { + const updateRequireLogin = async (requireLogin: boolean) => { + if (settings.hasPassword) { + setPendingRequireLoginVal(requireLogin); + setRequireLoginPassword(""); + setRequireLoginError(""); + setRequireLoginModalOpen(true); + return; + } + try { const res = await fetch("/api/settings", { method: "PATCH", @@ -34,13 +50,45 @@ export default function SecurityTab() { body: JSON.stringify({ requireLogin }), }); if (res.ok) { - setSettings((prev) => ({ ...prev, requireLogin })); + setSettings((prev: any) => ({ ...prev, requireLogin })); } } catch (err) { console.error("Failed to update require login:", err); } }; + const confirmRequireLoginUpdate = async () => { + if (pendingRequireLoginVal === null) return; + setRequireLoginLoading(true); + setRequireLoginError(""); + + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + requireLogin: pendingRequireLoginVal, + currentPassword: requireLoginPassword, + }), + }); + + if (res.ok) { + setSettings((prev: any) => ({ ...prev, requireLogin: pendingRequireLoginVal })); + setRequireLoginModalOpen(false); + } else { + const data = await res.json(); + setRequireLoginError( + data?.error?.message || t("errorOccurred", { fallback: "An error occurred" }) + ); + } + } catch (err) { + console.error("Failed to update require login:", err); + setRequireLoginError(t("errorOccurred", { fallback: "An error occurred" })); + } finally { + setRequireLoginLoading(false); + } + }; + const updateSetting = async (key: string, value: any) => { try { const res = await fetch("/api/settings", { @@ -87,6 +135,7 @@ export default function SecurityTab() { if (res.ok) { setPassStatus({ type: "success", message: t("passwordUpdated") }); setPasswords({ current: "", new: "", confirm: "" }); + setSettings((prev: any) => ({ ...prev, hasPassword: true })); } else { setPassStatus({ type: "error", message: data.error || t("failedUpdatePassword") }); } @@ -122,6 +171,49 @@ export default function SecurityTab() { disabled={loading} /> + + !requireLoginLoading && setRequireLoginModalOpen(false)} + title={t("currentPassword")} + > +
+

+ {t("enterCurrentPassword", { fallback: "Enter your current password to continue" })} +

+ setRequireLoginPassword(e.target.value)} + onKeyDown={(e) => + e.key === "Enter" && requireLoginPassword && confirmRequireLoginUpdate() + } + autoFocus + disabled={requireLoginLoading} + /> + {requireLoginError &&

{requireLoginError}

} +
+ + +
+
+
+ {settings.requireLogin === true && (
}; } - if (text.includes("GROUP BY 'direct'")) { + if (text.includes("GROUP BY combo_strategy")) { return { all: () => [ { diff --git a/tests/unit/dev-origins-config.test.ts b/tests/unit/dev-origins-config.test.ts index 59385da8cc..77c6e24c01 100644 --- a/tests/unit/dev-origins-config.test.ts +++ b/tests/unit/dev-origins-config.test.ts @@ -4,10 +4,5 @@ import assert from "node:assert/strict"; test("next config allows loopback dev origins alongside LAN access", async () => { const { default: nextConfig } = await import("../../next.config.mjs"); - assert.deepEqual(nextConfig.allowedDevOrigins, [ - "localhost", - "127.0.0.1", - "192.168.0.250", - "192.168.0.111", - ]); + assert.deepEqual(nextConfig.allowedDevOrigins, ["localhost", "127.0.0.1", "192.168.0.250"]); }); diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index 82aed99ef6..ae84ec41e0 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -31,7 +31,12 @@ test("next config exposes standalone build settings and canonical rewrites", asy assert.equal(nextConfig.distDir, ".next-task607"); assert.equal(nextConfig.output, "standalone"); assert.equal(nextConfig.images.unoptimized, true); - assert.deepEqual(nextConfig.transpilePackages, ["@omniroute/open-sse", "@lobehub/icons"]); + assert.deepEqual(nextConfig.transpilePackages, [ + "@omniroute/open-sse", + "@lobehub/icons", + "fumadocs-ui", + "fumadocs-core", + ]); assert.equal(headers[0].source, "/:path*"); assert.match(securityHeaders["Content-Security-Policy"], /default-src 'self'/); assert.match(securityHeaders["Content-Security-Policy"], /frame-ancestors 'none'/); @@ -102,6 +107,7 @@ test("next-intl webpack hook preserves caller config and filters known extractor nextConfig.webpack(config, { isServer: false, + defaultLoaders: { babel: {} } as any, webpack: { IgnorePlugin: class { options: any; diff --git a/tests/unit/providers-page-utils.test.ts b/tests/unit/providers-page-utils.test.ts index a9760f340a..31fa453024 100644 --- a/tests/unit/providers-page-utils.test.ts +++ b/tests/unit/providers-page-utils.test.ts @@ -15,14 +15,17 @@ test("merged OAuth providers keep free-tier providers in the OAuth section", () return { total: authType === "free" ? 1 : 0 }; }; + const mockOauthProviders = { claude: { name: "Claude" } }; + const mockFreeProviders = { "gemini-cli": { name: "Gemini CLI" } }; + const entries = providerPageUtils.buildMergedOAuthProviderEntries( - providers.OAUTH_PROVIDERS, - providers.FREE_PROVIDERS, + mockOauthProviders, + mockFreeProviders, getProviderStats ); - const oauthIds = Object.keys(providers.OAUTH_PROVIDERS); - const freeIds = Object.keys(providers.FREE_PROVIDERS); + const oauthIds = Object.keys(mockOauthProviders); + const freeIds = Object.keys(mockFreeProviders); assert.deepEqual( entries.slice(0, oauthIds.length).map((entry) => entry.providerId), @@ -34,6 +37,7 @@ test("merged OAuth providers keep free-tier providers in the OAuth section", () ); const freeEntry = entries.find((entry) => entry.providerId === freeIds[0]); + assert.ok(freeEntry, "Should find the free entry"); assert.equal(freeEntry.displayAuthType, "oauth"); assert.equal(freeEntry.toggleAuthType, "free"); assert.equal( @@ -263,8 +267,8 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre const museSparkWebProvider = providerPageUtils.resolveDashboardProviderInfo("muse-spark-web"); const upstreamProvider = providerPageUtils.resolveDashboardProviderInfo("cliproxyapi"); - assert.equal(freeProvider?.category, "free"); - assert.equal(freeProvider?.name, providers.FREE_PROVIDERS["amazon-q"].name); + assert.equal(freeProvider?.category, "oauth"); + assert.equal(freeProvider?.name, providers.OAUTH_PROVIDERS["amazon-q"].name); assert.equal(localProvider?.category, "local"); assert.equal(localProvider?.name, providers.LOCAL_PROVIDERS.sdwebui.name);