Skip to content

fix(auth): the browser-JWT branch now carries username, email and role - #1231

Open
lilyshen0722 wants to merge 1 commit into
mainfrom
fix/auth-jwt-branch-carries-username-and-role
Open

fix(auth): the browser-JWT branch now carries username, email and role#1231
lilyshen0722 wants to merge 1 commit into
mainfrom
fix/auth-jwt-branch-carries-username-and-role

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

middleware/auth.ts dispatches on the token prefix and leaves two different shapes on req.user:

branch assigns
cm_ API token (:51) { id, username, email, role }
browser JWT (:81) { id }

Nothing errors. Consumers just read undefined for every browser session.

Two live consequences, both confirmed at 799e0d7d

Why one fix rather than two

Three call sites had already grown a private DB re-read to work around this, each with its own comment rediscovering the same cause:

github.ts is the fourth site, and the one that never got its copy. Fixing it locally would make four.

The JWT branch already runs one indexed User.findById(id) per request for ban enforcement (#636). Widening that projection from banned to banned username email role makes the branches shape-identical at zero extra round-trips, so no consumer has to know which token type it was called with. The three existing local re-reads stay correct and become redundant fast-paths — removing them is separate work, deliberately not in this PR.

On the test

It drives the real middleware over a real JWT. The suites covering the affected routes cannot see this defect: their fake auth middleware injects req.user = { _id, role } directly — the API-token shape — so #809's /status tests pass today against the broken predicate.

The User mock honours the projection string, and that is load-bearing rather than tidiness. With a naive select() that ignores its argument and returns the whole fixture, narrowing the middleware back to .select('banned') leaves all seven tests green — the fields arrive from the fixture, not from the query. Verified by mutation, both ways:

mutation result
.select('banned username email role').select('banned') 4 of 7 red
req.user = { id, username, email, role }req.user = { id } 4 of 7 red

The CONTROL: case stays green under both, by design — it guards against over-granting (role: 'admin' hardcoded), not against the defect.

a user row without a role leaves role undefined, not defaulted pins that absence stays absent: a default here would grant or deny on data the row does not contain.

Verification

  • backend/__tests__/unit/middleware/ — 38/38, 7 suites
  • backend/__tests__/unit/routes + controllers — 650/650, 99 suites
  • tsc --noEmit — no errors in either touched file (pre-existing errors in scripts/ are untouched)
  • npm run lint — the two remaining errors on the new file are the repo-wide import/no-unresolved + import/extensions pattern that every sibling middleware test carries

Not verified: no runtime exercise of /api/github/status or the registry routes against a live browser session — both findings are source-level plus the middleware unit test. I also did not count how many registry rows actually lack publisher.name; no Mongo read path from this seat.

Relates to #1211 (which fixes the registry sites at the consumer) and #809 (which touches /status but leaves :146 unchanged).

🤖 Generated with Claude Code

`middleware/auth.ts` dispatched on the token prefix and left two different
shapes on `req.user`: the `cm_` API-token branch assigned
`{ id, username, email, role }`, the browser-JWT branch assigned `{ id }`.
Nothing errored — consumers simply read `undefined` for every browser session.

Two live consequences, both confirmed at 799e0d7:

  - `github.ts:146` refuses genuine admins with `403 Admin only`; the same
    admin holding an API token gets through.
  - registry publish/install persist `publisher.name: undefined` (#1211).

Three call sites had already grown a private DB re-read to work around it —
`podController.isGlobalAdminRequest`, `agentProfile.canEditAgentAvatar` and
`marketplace-api.resolveUsername`, the last of which #1211 duplicates rather
than moves. `github.ts` was the site that never got its copy.

The JWT branch already runs one indexed `User.findById` per request for ban
enforcement, so widening that projection makes the two branches shape-identical
at zero extra round-trips. The existing local re-reads stay correct and become
redundant fast-paths; removing them is separate work.

The test drives the real middleware over a real JWT, because the suites
covering the affected routes inject `{ _id, role }` from a fake auth
middleware — i.e. the API-token shape — and so cannot see this defect. Its
`User` mock honours the projection string: with a `select()` that ignores its
argument, narrowing the middleware back to `.select('banned')` leaves all seven
tests green. Both mutations (narrowed projection, restored `{ id }`) redden
four of seven.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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