fix: Move the API keys page off the /api prefix

/api-keys shares a raw string prefix with /api, and the proxies in front
of this app do not all match by path segment. Nginx Proxy Manager routes
/api straight to the Go server with a prefix location, so /api-keys never
reached Next at all — it reached a control plane with no such route and
came back as a JSON 404. Traefik's PathPrefix has the same shape of
matcher, which puts the Helm ingress at risk whenever ingress.api.enabled
is on.

The page is /tokens now, which cannot collide with anything, and which
matches the /api/tokens the REST API already publishes. The sidebar still
says API Keys — the label is for the reader, the path is for the router.

A permanent redirect covers anyone who bookmarked the old path today.
Fixing the proxy config instead would have left the trap set for the next
deployment, and for whatever sits in front of it.
This commit is contained in:
2026-08-13 09:26:34 +00:00
parent 18495dba68
commit 7e767ecb4f
5 changed files with 19 additions and 5 deletions
+2 -2
View File
@@ -467,7 +467,7 @@ window, answering 429 with `Retry-After`; cookie sessions are untouched; it
exists so a runaway script cannot take an instance down, not as the general
API rate-limiting project some future ticket might build.
**The UI calls them API keys and lives at `/api-keys`, not on `/settings`.**
**The UI calls them API keys and lives at `/tokens`, not on `/settings`.**
The page is reachable at **every** role, which is the whole reason it is a page:
`/settings` is owner|admin throughout, so a card there hid a capability every
member has. `settings.api_token_max_days` stays on `/settings` because it is
@@ -1022,7 +1022,7 @@ Customer nav is three destinations — Overview, People, Billing. Settings is in
| `/steps` | Reusable step library |
| `/monitors`, `/monitors/new`, `/monitors/[id][/edit]` | Checks, uptime, incidents |
| `/secrets`, `/secrets/[group]` | Vault |
| `/api-keys` | Personal API keys — reachable at **every** role, unlike `/settings` |
| `/tokens` | Personal API keys — reachable at **every** role, unlike `/settings` |
| `/audit` | Audit log |
| `/settings`, `/settings/notifications`, `/settings/license` | Members, OIDC, alerts, retention, ESO token · channels · licence |
+2 -2
View File
@@ -297,7 +297,7 @@ export default function SettingsPage() {
</SectionCard>
{/* The cap lives here rather than on /api-keys because it is
{/* The cap lives here rather than on /tokens because it is
instance policy, not one person's credentials — which is
also what lets that page be reachable at every role. */}
<SectionCard
@@ -313,7 +313,7 @@ export default function SettingsPage() {
</Field>
<p className="mt-4 text-sm text-text-secondary">
Keys themselves are managed on{" "}
<Link href="/api-keys" className="text-accent hover:underline">
<Link href="/tokens" className="text-accent hover:underline">
API Keys
</Link>
, which every member can reach.
+1 -1
View File
@@ -185,7 +185,7 @@ const navGroups: NavGroup[] = [
// Not adminOnly: the API lets any member mint and revoke their own
// keys, capped at their own role, so gating the page would hide a
// capability they have.
{ href: "/api-keys", label: "API Keys", icon: <TokenIcon /> },
{ href: "/tokens", label: "API Keys", icon: <TokenIcon /> },
],
},
{
+14
View File
@@ -8,6 +8,20 @@ const apiUrl = process.env.API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http:/
const nextConfig: NextConfig = {
output: "standalone",
async redirects() {
return [
{
// The API keys page briefly lived at /api-keys, which every
// raw-prefix proxy in front of this app captures with its /api
// rule — nginx's `location /api` matches /api-keys, so the
// request reached the Go server and 404'd. The page is at
// /tokens now precisely because that cannot happen to it.
source: "/api-keys",
destination: "/tokens",
permanent: true,
},
];
},
async rewrites() {
return [
{