fix(settings): fix Require Login modal Cancel button text and dismissal (#2649)

Integrated into release/v3.8.3
This commit is contained in:
Chewji
2026-05-24 13:14:21 +07:00
committed by GitHub
parent 69536917d0
commit 7e70255aab
9 changed files with 141 additions and 50 deletions

View File

@@ -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
};

View File

@@ -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<boolean | null>(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}
/>
</div>
<Modal
isOpen={requireLoginModalOpen}
onClose={() => !requireLoginLoading && setRequireLoginModalOpen(false)}
title={t("currentPassword")}
>
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">
{t("enterCurrentPassword", { fallback: "Enter your current password to continue" })}
</p>
<Input
label={t("currentPassword")}
type="password"
placeholder={t("currentPassword")}
value={requireLoginPassword}
onChange={(e) => setRequireLoginPassword(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" && requireLoginPassword && confirmRequireLoginUpdate()
}
autoFocus
disabled={requireLoginLoading}
/>
{requireLoginError && <p className="text-sm text-red-500">{requireLoginError}</p>}
<div className="flex justify-end gap-2 pt-2">
<Button
variant="ghost"
onClick={() => setRequireLoginModalOpen(false)}
disabled={requireLoginLoading}
>
{tc("cancel")}
</Button>
<Button
variant="primary"
onClick={confirmRequireLoginUpdate}
loading={requireLoginLoading}
disabled={!requireLoginPassword}
>
{t("confirm", { fallback: "Confirm" })}
</Button>
</div>
</div>
</Modal>
{settings.requireLogin === true && (
<form
onSubmit={handlePasswordChange}

View File

@@ -18,22 +18,6 @@ export type ServiceKind =
export type RiskNoticeVariant = "oauth" | "webCookie" | "deprecated";
/**
* Service kind — declarative tag for what a provider can do beyond basic LLM chat.
* Affects UI filtering only; does not influence request routing.
*/
export type ServiceKind =
| "llm"
| "embedding"
| "image"
| "imageToText"
| "tts"
| "stt"
| "webSearch"
| "webFetch"
| "video"
| "music";
export interface ProviderRiskNoticeFields {
subscriptionRisk?: boolean;
riskNoticeVariant?: RiskNoticeVariant;

View File

@@ -667,12 +667,14 @@ test("chatCore normalizes native Claude Code messages for native Claude OAuth pa
true
);
// user msg[0] (was clientMessages[1]): empty text stripped, document→text, future_block dropped
// Remaining: ["Run pwd" text, "[README.md]\nDo not flatten me" text]
assert.equal(call.body.messages[0].content.length, 2);
assert.equal(call.body.messages[0].content[0].text, "Run pwd");
assert.equal(call.body.messages[0].content[1].type, "text");
assert.equal(call.body.messages[0].content[1].text, "[README.md]\nDo not flatten me");
// user msg[0] (was clientMessages[1]): empty text, document and future_block are preserved
// since it is a semantic passthrough request
assert.equal(call.body.messages[0].content.length, 4);
assert.equal(call.body.messages[0].content[0].type, "text");
assert.equal(call.body.messages[0].content[0].text, "");
assert.equal(call.body.messages[0].content[1].text, "Run pwd");
assert.equal(call.body.messages[0].content[2].type, "document");
assert.equal(call.body.messages[0].content[3].type, "future_block");
// assistant msg[1] (was clientMessages[2]): tool_use unchanged
assert.equal(call.body.messages[1].content[0].type, "tool_use");
@@ -789,12 +791,14 @@ test("chatCore normalizes native Claude Code messages before CC-compatible relay
true
);
// user msg[0] (was clientMessages[1]): empty text stripped, document→text, future_block dropped
// Remaining: ["Inspect project" text, "[design.md]\nKeep as document block" text]
assert.equal(call.body.messages[0].content.length, 2);
assert.equal(call.body.messages[0].content[0].text, "Inspect project");
assert.equal(call.body.messages[0].content[1].type, "text");
assert.equal(call.body.messages[0].content[1].text, "[design.md]\nKeep as document block");
// user msg[0] (was clientMessages[1]): empty text, document and future_block are preserved
// since it is a semantic passthrough request
assert.equal(call.body.messages[0].content.length, 4);
assert.equal(call.body.messages[0].content[0].type, "text");
assert.equal(call.body.messages[0].content[0].text, "");
assert.equal(call.body.messages[0].content[1].text, "Inspect project");
assert.equal(call.body.messages[0].content[2].type, "document");
assert.equal(call.body.messages[0].content[3].type, "future_block");
// assistant msg[1] (was clientMessages[2]): tool_use unchanged
assert.equal(call.body.messages[1].content[0].type, "tool_use");

View File

@@ -15,8 +15,11 @@ const BIN = path.join(
);
function runCli(dataDir: string): { code: number | null; stderr: string } {
const cleanEnv = { ...process.env };
delete cleanEnv.STORAGE_ENCRYPTION_KEY;
const res = spawnSync("node", [BIN, "--help"], {
env: { ...process.env, DATA_DIR: dataDir, NO_UPDATE_NOTIFIER: "1" },
cwd: dataDir,
env: { ...cleanEnv, DATA_DIR: dataDir, NO_UPDATE_NOTIFIER: "1" },
timeout: 60_000,
encoding: "utf-8",
});

View File

@@ -844,7 +844,7 @@ test("cache metrics and trend coerce null aggregate fields to zero", async () =>
};
}
if (text.includes("GROUP BY 'direct'")) {
if (text.includes("GROUP BY combo_strategy")) {
return {
all: () => [
{

View File

@@ -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"]);
});

View File

@@ -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;

View File

@@ -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);