mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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:
@@ -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">
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user