fix(api-manager): preserve API key expiration local time (#3146)

Preserve API key expiration local time + clear button. Integrated into release/v3.8.10.
This commit is contained in:
Xiangzhe
2026-06-05 01:10:41 +08:00
committed by GitHub
parent 32bb4b9652
commit 3fd8f2ce11
4 changed files with 59 additions and 21 deletions

View File

@@ -30,6 +30,16 @@ import {
const MAX_KEY_NAME_LENGTH = 200;
const MAX_SELECTED_MODELS = 500;
function toLocalDateTimeInputValue(value: string | null | undefined): string {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const pad = (n: number) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
date.getHours()
)}:${pad(date.getMinutes())}`;
}
// Debounce hook for search optimization
function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -2128,15 +2138,32 @@ const PermissionsModal = memo(function PermissionsModal({
Key will automatically stop working after this date.
</p>
</div>
<input
type="datetime-local"
value={expiresAt ? expiresAt.slice(0, 16) : ""}
onChange={(e) => {
const val = e.target.value;
setExpiresAt(val ? new Date(val).toISOString() : "");
}}
className="w-full px-2 py-1.5 text-sm border border-border rounded-md bg-background text-text-main"
/>
<div className="flex gap-2">
<input
type="datetime-local"
value={toLocalDateTimeInputValue(expiresAt)}
onChange={(e) => {
const val = e.target.value;
if (!val) {
setExpiresAt("");
return;
}
const date = new Date(val);
if (!Number.isNaN(date.getTime())) {
setExpiresAt(date.toISOString());
}
}}
className="min-w-0 flex-1 px-2 py-1.5 text-sm border border-border rounded-md bg-background text-text-main"
/>
<button
type="button"
onClick={() => setExpiresAt("")}
disabled={!expiresAt}
className="shrink-0 px-3 py-1.5 text-sm font-medium border border-border rounded-md text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
>
{tc("clear")}
</button>
</div>
</div>
{/* Management Access */}
<div className="flex flex-col gap-2 p-3 rounded-lg border border-border bg-surface/40">

View File

@@ -35,6 +35,21 @@ test("permissions modal uses i18n for management access description", () => {
assert.doesNotMatch(managementBlock, /Allow this API key to manage OmniRoute configuration\./);
});
test("permissions modal converts API key expiration ISO timestamps to local datetime input values", () => {
const source = readApiManagerPage();
const expirationBlock = source.slice(
source.indexOf("{/* Expiration Date */}", source.indexOf("const PermissionsModal")),
source.indexOf("{/* Management Access */}", source.indexOf("const PermissionsModal"))
);
assert.match(expirationBlock, /value=\{toLocalDateTimeInputValue\(expiresAt\)\}/);
assert.match(expirationBlock, /const date = new Date\(val\)/);
assert.match(expirationBlock, /setExpiresAt\(date\.toISOString\(\)\)/);
assert.match(expirationBlock, /onClick=\{\(\) => setExpiresAt\(""\)\}/);
assert.match(expirationBlock, /\{tc\("clear"\)\}/);
assert.doesNotMatch(expirationBlock, /expiresAt\.slice\(0, 16\)/);
});
test("permissions modal switch buttons declare button type", () => {
const source = readApiManagerPage();
const modalStart = source.indexOf("const PermissionsModal");

View File

@@ -1,6 +1,5 @@
import { afterEach, describe, it } from "node:test";
import { describe, it } from "node:test";
import assert from "node:assert";
import { deleteFile, listFiles } from "@/lib/localDb";
// Helper functions from batchProcessor.ts
function parseBatchWindowSeconds(window: string | null | undefined): number {
@@ -18,15 +17,6 @@ function parseBatchWindowSeconds(window: string | null | undefined): number {
}
describe("Batch Processor - File Expiration", () => {
afterEach(() => {
const allFiles = listFiles({ limit: 1000 });
for (const f of allFiles) {
if (f.filename?.includes("test-batch-")) {
deleteFile(f.id);
}
}
});
describe("getBatchOutputExpiresAt logic", () => {
it("should calculate output expiration as 30 days from batch completion", () => {
const completedAt = Math.floor(Date.now() / 1000) - 5 * 24 * 60 * 60; // 5 days ago

View File

@@ -8,7 +8,13 @@ import { updateSettings } from "../../src/lib/db/settings";
const TEST_LOG_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-console-log-levels-"));
const TEST_LOG_PATH = path.join(TEST_LOG_DIR, "app.log");
process.once("exit", () => fs.rmSync(TEST_LOG_DIR, { recursive: true, force: true }));
process.once("exit", () => {
try {
fs.rmSync(TEST_LOG_DIR, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
} catch {
// Best-effort cleanup only; tmpdir residue must not fail otherwise-passing tests.
}
});
const originalLogFilePath = process.env.APP_LOG_FILE_PATH;
process.env.APP_LOG_FILE_PATH = TEST_LOG_PATH;