mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
fix(settings): fix Require Login modal Cancel button text and dismissal (#2649)
Integrated into release/v3.8.3
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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: () => [
|
||||
{
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user