Skip to content

feat(admin): validate and document the window on the user list - #299

Merged
Bccorb merged 1 commit into
mainfrom
feat/admin-users-query-schema
Sep 8, 2026
Merged

feat(admin): validate and document the window on the user list#299
Bccorb merged 1 commit into
mainfrom
feat/admin-users-query-schema

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes the caveat that fells-code/seamless-auth-docs#42 had to write down: GET /admin/users was the one admin collection reading query parameters it did not declare.

What changed

The route has always read limit, offset and search, but declared no query schema, so the parameters were absent from openapi.json. A generated client could not know they existed, and a reader checking the document would conclude the endpoint took none. That is the same wrong inference that led to organization paging being reported missing in the first place.

They are declared now, so the generated contract lists them alongside /admin/sessions, /admin/auth-events and /admin/organizations, and they are validated the same way: limit 1 to 100 defaulting to 50, offset from 0, search trimmed.

Three inputs that used to be accepted now return 400

  • limit above 100 was honoured in full, so one call could ask for every user in the deployment.
  • A non-numeric limit reached Sequelize as NaN and failed in the database rather than at the edge.
  • An all-whitespace search built a %% pattern matching every row, so a filter that looked empty returned the unfiltered list.

seamless-cli accepts users list --limit 0 deliberately, meaning ask for nothing, and that value is now a 400. The changeset says so. The flag needs a floor of 1, or to skip the request when asked for zero rows. I have not touched the CLI.

I kept the shared PaginationQuerySchema bounds rather than special-casing this route to allow limit=0, since a route that accepts a window the other three reject would recreate exactly the inconsistency this change removes.

Something worth knowing before this ships

While checking which callers send a large limit, I found that neither adapter forwards the query string on GET /admin/users. It is the only admin list route they drop it for. Verified empirically against the built Express adapter:

/admin/users         -> https://auth.example.com/admin/users
/admin/sessions      -> https://auth.example.com/admin/sessions?limit=10&offset=20&search=ada
/admin/auth-events   -> https://auth.example.com/admin/auth-events?limit=10&offset=20&search=ada
/admin/organizations -> https://auth.example.com/admin/organizations?limit=10&offset=20&search=ada

In packages/core/src/handlers/admin.ts the users handler takes BaseOpts where the other list handlers take WithQuery, and neither packages/express/src/handlers/admin.ts nor the Fastify route table passes query for it.

The consequence is that the admin dashboard's user search and paging do nothing in production: it sends ?search=...&limit=10&offset=N, the adapter forwards a bare /admin/users, and the API answers with the default first 50. Its e2e tests pass because the Playwright mock intercepts at the dashboard boundary and never exercises the adapter.

That is an adapter fix, in its own PR in seamless-auth-server, and it is not blocked by this one. This change is a prerequisite for it being meaningful, and it is safe to land first: through the adapter no query arrives today, so nothing sees the new validation until the adapter starts forwarding.

Verification

  • typecheck, lint, format:check, build clean
  • test:run: 114 files, 1374 passing (up from 1364)
  • coverage passes the 98 percent line and 95 percent branch thresholds
  • openapi.json regenerated; GET /admin/users now reports ['limit', 'offset', 'search']

The route has always read limit, offset and search, but it declared no query
schema, so it was the one admin collection whose parameters were absent from
openapi.json. A generated client could not know they existed, and a reader
checking the document would conclude the endpoint took none. That is the same
wrong inference that led to organization paging being reported missing.

The parameters are declared now, so they appear in the generated contract
alongside the ones on /admin/sessions, /admin/auth-events and
/admin/organizations, and they are validated the same way: limit between 1 and
100 defaulting to 50, offset from 0, and search trimmed.

Three inputs that used to be accepted are rejected with a 400. A limit above 100
was honoured in full, so one call could ask for every user in the deployment. A
non-numeric limit reached Sequelize as NaN and failed in the database rather
than at the edge. An all-whitespace search built a %% pattern matching every
row, so a filter that looked empty returned the unfiltered list.

seamless-cli accepts users list --limit 0 deliberately, meaning ask for nothing,
and that value is now a 400. The changeset says so, since the flag needs a floor
of 1 or to skip the request when asked for zero rows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant