Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions src/app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
import { extractApiKey } from "@/sse/services/auth";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";

/**
* Force this route to run dynamically per-request and never be cached/prerendered.
* Combined with the `Cache-Control: no-store` response header below, this keeps
* persisted settings (e.g. dashboard preferences, debugMode, hidden sidebar
* items) visible immediately after refresh or restart instead of falling back
* to stale Next.js fetch cache. Ported from upstream decolua/9router#951.
*/
export const dynamic = "force-dynamic";
export const revalidate = 0;

/** Response headers applied to every successful GET/PATCH on /api/settings. */
const SETTINGS_RESPONSE_HEADERS = { "Cache-Control": "no-store" } as const;

/**
* Settings keys whose change broadens attack surface. Spec §Security:
* password re-auth is required when any of these is present in a PATCH body.
Expand Down Expand Up @@ -162,19 +175,22 @@ export async function GET(request: Request) {
// best effort — don't fail GET /api/settings if this lookup fails
}

return NextResponse.json({
...safeSettings,
hasPassword: hasManagementPasswordConfigured(settings),
runtimePorts,
apiPort: runtimePorts.apiPort,
dashboardPort: runtimePorts.dashboardPort,
cloudConfigured: Boolean(cloudUrl),
cloudUrl,
machineId,
...(cliproxyapiModelMapping !== null
? { cliproxyapi_model_mapping: cliproxyapiModelMapping }
: {}),
});
return NextResponse.json(
{
...safeSettings,
hasPassword: hasManagementPasswordConfigured(settings),
runtimePorts,
apiPort: runtimePorts.apiPort,
dashboardPort: runtimePorts.dashboardPort,
cloudConfigured: Boolean(cloudUrl),
cloudUrl,
machineId,
...(cliproxyapiModelMapping !== null
? { cliproxyapi_model_mapping: cliproxyapiModelMapping }
: {}),
},
{ headers: SETTINGS_RESPONSE_HEADERS }
);
} catch (error) {
console.log("Error getting settings:", error);
return NextResponse.json({ error: "Failed to load settings" }, { status: 500 });
Expand Down Expand Up @@ -376,7 +392,7 @@ export async function PATCH(request: Request) {
}

const { password, ...safeSettings } = settings;
return NextResponse.json(safeSettings);
return NextResponse.json(safeSettings, { headers: SETTINGS_RESPONSE_HEADERS });
} catch (error) {
console.log("Error updating settings:", error);
return NextResponse.json({ error: "Failed to update settings" }, { status: 500 });
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/settings-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,35 @@ describe("Settings API - persisted preferences", () => {
assert.equal(settings.responsesPreviousResponseIdMode, "strip");
});

test("GET /api/settings returns Cache-Control: no-store (ported from upstream #951)", async () => {
const response = await harness.settingsRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "GET",
})
);
assert.equal(response.status, 200);
assert.equal(
response.headers.get("Cache-Control"),
"no-store",
"GET /api/settings must return Cache-Control: no-store so persisted settings stay fresh after refresh/restart"
);
});

test("PATCH /api/settings returns Cache-Control: no-store (ported from upstream #951)", async () => {
const response = await harness.settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: { debugMode: true },
})
);
assert.equal(response.status, 200);
assert.equal(
response.headers.get("Cache-Control"),
"no-store",
"PATCH /api/settings must return Cache-Control: no-store"
);
});

test("PUT /api/settings reuses the PATCH update flow", async () => {
const response = await harness.settingsRoute.PUT(
await makeManagementSessionRequest("http://localhost/api/settings", {
Expand Down
Loading