Skip to content

1.8.5: accept the nest-server roles: string[] shape in isAdmin so the admin UI stops silently disappearing#5

Merged
kaihaase merged 1 commit into
mainfrom
fix/is-admin-roles-array
Jul 15, 2026
Merged

1.8.5: accept the nest-server roles: string[] shape in isAdmin so the admin UI stops silently disappearing#5
kaihaase merged 1 commit into
mainfrom
fix/is-admin-roles-array

Conversation

@kaihaase

@kaihaase kaihaase commented Jul 13, 2026

Copy link
Copy Markdown
Member

Problem

isAdmin currently checks the singular role field only:

const isAdmin = computed<boolean>(() => user.value?.role === 'admin');

But the lt nest-server / Better-Auth user carries its roles as an arrayroles: string[] — and has no role field at all. Against a real lt backend, isAdmin is therefore permanently false: the entire admin UI silently disappears for every admin, with no error and no log line.

This is not hypothetical. Verified live against a production instance — get-session returns:

{ "roles": ["admin"], "email": "", "id": "" }

— no role. In nest-server, roles is a core Better-Auth additional field, registered unconditionally with type: 'string[]' and defaultValue: [] (not optional, not opt-in). Every lt project with an admin UI is affected.

Fix

  • LtUser gains roles?: string[] as a real, typed field.
  • isAdmin accepts both shapes (singular role string and roles array), so it stays backward compatible.
  • Array.isArray guard: the auth-state cookie is client-writable, so a tampered roles: 42 must yield false rather than throw inside a computed.
  • roles is added to AUTHZ_KEYS. Once isAdmin reads roles, a stale cached roles: ['admin'] would keep the admin UI alive after the backend revoked the role — exactly the fail-open that AUTHZ_KEYS exists to prevent. Fail-closing role while fail-opening roles, when both feed the same isAdmin, would be incoherent.
    A backend that never sends roles loses nothing: AUTHZ_KEYS only drops a key when the cache has it and the session omits it — a roles-less backend never caches roles, so the drop is a no-op.

Tests

New test/is-admin.test.ts (8 cases): {roles:['admin']} without roletrue, {role:'admin'}true, {roles:['user']}false, nullfalse, plus tampered non-array values.
Extended test/merge-session-user.test.ts with 3 roles AUTHZ cases.

Counter-checked: with the old one-liner and the old AUTHZ_KEYS, exactly 4 of the new assertions fail. A regression test that passes against the broken code would be worthless.

Verification

format:check clean · lint exit 0 · test:types exit 0 · 128/128 tests (11 files) · build exit 0.

Note

The version bump to 1.8.5 follows this repo's convention (the PR commit does the bump; the maintainer adds the changelog entry separately on main after the merge — same as #4). If another PR lands first and this collides, that single hunk is trivial to drop.

…he admin UI stops silently disappearing

`isAdmin` was `user.value?.role === 'admin'` — Better-Auth's admin-plugin shape
(a singular `role` string) only. But `@lenne.tech/nest-server` registers `roles`
as a CORE Better-Auth additionalField (`type: 'string[]'`, `defaultValue: []`),
so its users carry `roles: ['admin']` and NO singular `role` at all.

Against a real nest-server backend `isAdmin` was therefore permanently `false`:
every `v-if="isAdmin"` branch (admin nav, admin pages) vanished for actual
admins, with no error anywhere — `undefined === 'admin'` is simply false. This
was verified against a live nest-server deployment whose admin user returns
`roles: ["admin"]` and no `role` field.

Changes:
- `LtUser` gains `roles?: string[]`, so the multi-role shape is a first-class,
  typed field rather than something consumers have to cast in.
- `isAdmin` accepts EITHER shape (`role === 'admin'` OR `roles` includes
  'admin'). `Array.isArray` guards a malformed `roles` in the client-writable
  auth-state cookie: junk yields `false` instead of throwing in the computed.
- `roles` joins `AUTHZ_KEYS`, so the session merge treats it exactly like
  `role`. It is authorization-relevant now that `isAdmin` reads it: a stale
  cached `roles: ['admin']` would otherwise keep the admin UI alive after the
  backend revoked admin — the precise fail-open that list exists to prevent.
  Fail-closing costs nothing for backends that never send `roles`, because a key
  is only dropped when the CACHE has it and the session omits it — a
  `roles`-less backend never caches `roles`, so it is a no-op. And nest-server
  always returns `roles` from get-session (core additionalField), so its worst
  case is an honest `[]` (= not an admin), never a spurious drop.

Tests: new `test/is-admin.test.ts` covers both shapes, multi-role arrays, empty
arrays, absent user and malformed `roles`; `test/merge-session-user.test.ts`
gains the `roles` fail-closed / downgrade / no-spurious-drop cases. All 4 new
behavioural assertions were confirmed to fail against the pre-fix code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kaihaase
kaihaase marked this pull request as draft July 13, 2026 14:06
@kaihaase kaihaase changed the title fix(auth): isAdmin akzeptiert die nest-server roles: string[]-Form 1.8.5: accept the nest-server roles: string[] shape in isAdmin so the admin UI stops silently disappearing Jul 13, 2026
@kaihaase
kaihaase merged commit 665995c into main Jul 15, 2026
1 check passed
@kaihaase
kaihaase deleted the fix/is-admin-roles-array branch July 15, 2026 13:21
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