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
2 changes: 1 addition & 1 deletion FRAMEWORK-API.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @lenne.tech/nest-server — Framework API Reference

> Auto-generated from source code on 2026-05-31 (v11.26.0)
> Auto-generated from source code on 2026-05-31 (v11.26.1)
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code

## CoreModule.forRoot()
Expand Down
193 changes: 193 additions & 0 deletions migration-guides/11.26.0-to-11.26.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Migration Guide: 11.26.0 → 11.26.1

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | None |
| **Bugfixes** | BetterAuth REST sign-in/sign-up/get-session responses now include the user's `roles` from the synced legacy user (previously silently dropped) |
| **New Features** | New optional field `roles?: string[]` on `CoreBetterAuthUserResponse` (additive, Swagger-documented) |
| **Migration Effort** | 0 minutes (automatic) — optional cleanup if your project shipped a `mapUser()` override as a workaround |

---

## Quick Migration

No code changes required. The fix activates automatically.

```bash
# Update package
pnpm add @lenne.tech/nest-server@11.26.1

# Verify build
pnpm run build

# Run tests
pnpm test
```

---

## What's Fixed in 11.26.1

### `roles` is now exposed on BetterAuth REST auth responses

**Affects:** Every project that uses BetterAuth (IAM) and reads `response.user` from the
REST endpoints `POST /iam/sign-in/email`, `POST /iam/sign-up/email`, or `GET /iam/session`.

**Symptom (before 11.26.1):**
The sign-in response body looked like this regardless of the user's role assignment:

```json
{
"success": true,
"user": {
"id": "ba-user-1",
"email": "admin@example.com",
"emailVerified": true,
"name": "Admin"
},
"token": "..."
}
```

Frontend consumers (e.g. `useLtAuth().setUser()` in `@lenne.tech/nuxt-extensions`,
the `lt-auth-state` cookie cache) persisted a roles-less user. Client-side admin gating
(e.g. routing the `nest-server-starter` setup flow into `/admin/*` based on
`roles: ['admin']`) silently failed for every freshly authenticated user.

**Fix (in 11.26.1):**
`CoreBetterAuthController.mapUser()` now forwards `roles` from the DB-synced legacy user:

```json
{
"success": true,
"user": {
"id": "ba-user-1",
"email": "admin@example.com",
"emailVerified": true,
"name": "Admin",
"roles": ["admin"]
},
"token": "..."
}
```

**Where the roles come from:**

`CoreBetterAuthController.mapUser()` reads `roles` from the result of
`CoreBetterAuthUserMapper.mapSessionUser()`, which looks the user up in the
`users` collection (`findOne({ $or: [{ email }, { iamId }] })`) and returns
`Array.isArray(dbUser.roles) ? dbUser.roles : []`. The client cannot influence
the value — roles remain server-authoritative.

**Defensive defaults:**

| `mappedUser` value | Response `roles` |
|-----------------------------------|------------------|
| `{ roles: ['admin'] }` | `['admin']` |
| `{ roles: [] }` | `[]` |
| `null` / `undefined` | `[]` |
| `{ roles: 'admin' }` (not array) | `[]` |
| `{}` (no `roles` field) | `[]` |

---

## Compatibility Notes

- **Frontend consumers (additive change):** Frontends that previously ignored
`response.user.roles` because it was always missing continue to work. Frontends
that now want to use it (admin gating, role-based UI) can read it directly —
no extra `/api/users/me` round-trip required after login.
- **Swagger / OpenAPI clients:** The generated schema gains `roles?: string[]`
on `CoreBetterAuthUserResponse`. Regenerate any SDK that consumes the OpenAPI
spec (e.g. `nuxt-base-starter` runs `pnpm run generate:types` against the API
schema).
- **Existing project-level `mapUser()` overrides (optional cleanup):**
If your project shipped a `mapUser()` override solely to add `roles` to the
response as a workaround, you can now delete it. See **Cleanup** below.
- **Subclasses that override `mapUser()`:** The second positional parameter is
unchanged in shape but was renamed `_mappedUser` → `mappedUser` (the leading
underscore previously signalled "unused"). Existing overrides keep working —
parameter names are local to each function signature.
- **Projects without BetterAuth:** Not affected. Legacy `signIn` GraphQL/REST
responses already included `user.roles` and are unchanged.

---

## Cleanup (Optional)

If your project previously added a `mapUser()` override in its
`BetterAuthController` subclass solely to surface `roles`, the override is now
redundant and can be removed. Example pattern that can be deleted:

```typescript
// Project: src/server/modules/better-auth/better-auth.controller.ts
//
// REMOVE this override — the core now does the same thing.
protected override mapUser(sessionUser: BetterAuthSessionUser, mappedUser: any) {
return {
...super.mapUser(sessionUser, mappedUser),
roles: Array.isArray(mappedUser?.roles) ? mappedUser.roles : [],
};
}
```

Keep the override if it adds project-specific fields beyond `roles` (e.g.
`status`, `type`, `avatar`). In that case, drop only the `roles` line — `roles`
is now part of the base response.

---

## Troubleshooting

### `roles` is still missing in the response

Check, in this order:

1. **You actually pulled the new version.** Run `pnpm list @lenne.tech/nest-server`
and confirm 11.26.1 (or via the upstream-core-updater agent for vendored projects).
2. **The user has been synced to the `users` collection.** `mapSessionUser()` looks
up the user via `$or: [{ email }, { iamId }]`. A user signed up exclusively via
BetterAuth but never created in the legacy `users` collection will fall through
to the "no DB user" branch which returns `roles: []` — assign roles in the
`users` collection (e.g. via `CoreUserService.setRoles()`), not in the
`iam_user` collection.
3. **The role is a `S_`-prefixed system role.** Per project rule, `S_USER`,
`S_VERIFIED`, `S_CREATOR`, `S_SELF` etc. are runtime checks and must never be
stored in `user.roles`. They will not appear in the response.
4. **A custom `mapUser()` override strips `roles`.** Search your project for
`mapUser` and confirm the override either calls `super.mapUser(...)` or
forwards `roles` explicitly.
5. **The user-mapper cache is stale.** In production the mapper caches DB lookups
for 15s per `iamId`. `CoreUserService.setRoles()` / `update()` already invalidate
the cache; manual DB writes do not. Force a re-read by calling
`userMapper.invalidateUserCache(iamId)` after a direct write, or wait 15s.

### Swagger schema shows `roles` as `required`

It is not — the property is declared as `required: false`. If your generated
client marks it required, regenerate after pulling 11.26.1.

---

## Module Documentation

### BetterAuth Module

- **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
- **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
- **Customization Guide:** [src/core/modules/better-auth/CUSTOMIZATION.md](../src/core/modules/better-auth/CUSTOMIZATION.md) — patterns for overriding controller / resolver
- **Reference Implementation:** `src/server/modules/better-auth/`
- **Key Files:**
- `src/core/modules/better-auth/core-better-auth.controller.ts` — `mapUser()` (the fix), `CoreBetterAuthUserResponse` (the response DTO with the new `roles` field)
- `src/core/modules/better-auth/core-better-auth-user.mapper.ts` — `mapSessionUser()` (source of `roles`, with 15s TTL cache and `invalidateUserCache()`)

---

## References

- [Role System](../.claude/rules/role-system.md) — Real roles vs. `S_`-prefixed runtime checks
- [Module Inheritance Pattern](../.claude/rules/module-inheritance.md) — How to extend `CoreBetterAuthController.mapUser()` cleanly
- [Migration Guide 11.25.x → 11.26.0](./11.25.x-to-11.26.0.md) — Previous release
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lenne.tech/nest-server",
"version": "11.26.0",
"version": "11.26.1",
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
"keywords": [
"node",
Expand Down
2 changes: 1 addition & 1 deletion spectaql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ servers:
info:
title: lT Nest Server
description: Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).
version: 11.26.0
version: 11.26.1
contact:
name: lenne.Tech GmbH
url: https://lenne.tech
Expand Down
23 changes: 20 additions & 3 deletions src/core/modules/better-auth/core-better-auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ export class CoreBetterAuthUserResponse {
@ApiProperty({ description: 'User display name' })
name: string;

@ApiProperty({
description: 'Roles of the user (e.g. ["admin"]) — populated from the synced legacy user.',
isArray: true,
required: false,
type: String,
})
roles?: string[];

@ApiProperty({ description: 'Whether 2FA is enabled', required: false })
twoFactorEnabled?: boolean;
}
Expand Down Expand Up @@ -799,16 +807,25 @@ export class CoreBetterAuthController {

/**
* Map user to response format
*
* The `mappedUser` parameter is the synced legacy user from `mapSessionUser()` — it
* carries DB-only fields like `roles` that are not part of the Better-Auth session
* payload but are required by consumers for admin gating / RBAC. The base
* implementation forwards `roles` so the client-side state (`useLtAuth().setUser()`
* cache, `lt-auth-state` cookie) can route admin areas without a second round-trip.
*
* Subclasses can override to add project-specific fields (e.g. `status`, `type`).
*
* @param sessionUser - The user from Better-Auth session
* @param _mappedUser - The synced user from legacy system (available for override customization)
* @param mappedUser - The synced user from legacy system (includes `roles`)
*/

protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
protected mapUser(sessionUser: BetterAuthSessionUser, mappedUser: any): CoreBetterAuthUserResponse {
return {
email: sessionUser.email,
emailVerified: sessionUser.emailVerified || false,
id: sessionUser.id,
name: sessionUser.name || sessionUser.email.split('@')[0],
roles: Array.isArray(mappedUser?.roles) ? mappedUser.roles : [],
};
}

Expand Down
Loading
Loading