# Migration Guide: 11.20.x → 11.21.0

## Overview

| Category | Changes | Effort |
|----------|---------|--------|
| **Breaking** | `userField` removed from `IMultiTenancy`, tenant filtering behavior changed | Medium |
| **Breaking** | Safety Net: tenantId-schemas throw `ForbiddenException` without valid tenant context | Low |
| **New Feature** | Header-based multi-tenant membership with configurable hierarchy roles | Low-Medium |

## Not Using Multi-Tenancy?

If your project does **not** configure `multiTenancy` in `config.env.ts`, this update requires **no action**. All changes are fully backward compatible — just update the package version.

## Breaking Changes

### 1. `userField` Removed from IMultiTenancy

The `userField` property has been removed. Tenant ID is now read from the `X-Tenant-Id` request header instead of from `req.user`.

**Before (11.20.x):**
```typescript
multiTenancy: {
  userField: 'organizationId',
  excludeSchemas: ['User'],
}
```

**After (11.21.0):**
```typescript
multiTenancy: {
  headerName: 'x-tenant-id', // Optional, this is the default
  excludeSchemas: ['User'],
}
```

**Action Required:** Remove any `userField` references from your `config.env.ts`.

### 2. Tenant Filtering Behavior Change

**Before (11.20.x):**
- Logged-in user without `tenantId` → queries filtered by `{ tenantId: null }` (sees only unassigned data)

**After (11.21.0):**
- Unauthenticated request with `X-Tenant-Id` header → **403 "Authentication required for tenant access"**
- Authenticated request without `X-Tenant-Id` header + hierarchy role → checks `user.roles`, filters `tenantIds` by level
- Authenticated user with no memberships → **Safety Net: `ForbiddenException`** on tenantId-schemas
- Use `@Roles(DefaultHR.MEMBER)` to require tenant membership on specific endpoints

**Action Required:** If you relied on the null-filter behavior, use `@Roles(DefaultHR.MEMBER)` on endpoints that should require tenant context.

### 3. Safety Net (Defense-in-Depth)

The Mongoose tenant plugin now throws `ForbiddenException` instead of returning unfiltered data when a tenantId-scoped schema is accessed without valid tenant context.

**Affected scenarios:**
- Public endpoints (`@Roles(S_EVERYONE)`) accessing tenantId-scoped models → now throw 403
- System operations without `RequestContext` → unaffected (no context = no filter, as before)
- Operations with `bypassTenantGuard` → unaffected (bypass skips all filtering)

**Action Required:** Public routes that access tenantId-scoped models must use one of:
- `@SkipTenantCheck()` decorator + `RequestContext.runWithBypassTenantGuard()` in the service
- Or ensure a valid tenant context is provided

### 4. New IMultiTenancy Properties

```typescript
export interface IMultiTenancy {
  enabled?: boolean;
  excludeSchemas?: string[];
  // NEW
  headerName?: string;           // Default: 'x-tenant-id'
  membershipModel?: string;      // Default: 'TenantMember'
  adminBypass?: boolean;          // Default: true
  roleHierarchy?: Record<string, number>;  // Default: { member: 1, manager: 2, owner: 3 }
}
```

## New Feature: Multi-Tenant Membership

### What's New

- **CoreTenantModule** — Auto-registered when `multiTenancy` is configured
- **CoreTenantMemberModel** — Join table for user-tenant membership with roles
- **CoreTenantGuard** — APP_GUARD validating tenant header and membership
- **CoreTenantService** — Membership CRUD operations
- **Hierarchy Roles** — Configurable role levels with level comparison (`@Roles(DefaultHR.MEMBER)`)
- **Normal Roles** — Non-hierarchy roles with exact match (`@Roles('auditor')`)
- **`createHierarchyRoles()`** — Generate type-safe constants from hierarchy config
- **`DefaultHR`** — Pre-built constants for default hierarchy (MEMBER, MANAGER, OWNER)
- **`@SkipTenantCheck()`** — Skip tenant validation for specific endpoints
- **`@CurrentTenant()`** — Parameter decorator for tenant ID extraction
- **`TenantMemberStatus`** enum — ACTIVE, INVITED, SUSPENDED

### Quick Setup

```typescript
// config.env.ts — enable tenant system
multiTenancy: {},
```

### Protecting Endpoints

```typescript
import { Roles, CurrentTenant, DefaultHR } from '@lenne.tech/nest-server';

@Controller('projects')
export class ProjectController {
  @Get()
  @Roles(DefaultHR.MEMBER)  // any active member
  async list(@CurrentTenant() tenantId: string) {
    return this.projectService.find({ tenantId });
  }

  @Delete(':id')
  @Roles(DefaultHR.OWNER)  // highest level only
  async delete(@Param('id') id: string) {
    return this.projectService.delete(id);
  }
}
```

### Custom Hierarchy (5+ Levels)

```typescript
// config.env.ts
multiTenancy: {
  roleHierarchy: { viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 },
}

// roles.ts
import { createHierarchyRoles } from '@lenne.tech/nest-server';
export const HR = createHierarchyRoles({ viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 });

// resolver.ts
@Roles(HR.EDITOR)  // requires level >= 2 (editor, manager, admin, owner)
```

### Tenant Context Rule

When `X-Tenant-Id` header is present: only `membership.role` is checked (user.roles ignored, except ADMIN bypass).
When no header: `user.roles` is checked instead (same hierarchy/normal semantics).

### Frontend: Send Tenant Header

All tenant-scoped API calls must include:
```
X-Tenant-Id: <tenant-id>
```

### How It Works

1. `RequestContextMiddleware` provides lazy getters for tenant context
2. `CoreTenantGuard` validates membership → sets `req.tenantId` (only after successful check)
3. Mongoose tenant plugin reads `tenantId` from `RequestContext` → filters queries
4. **Safety Net:** Plugin throws `ForbiddenException` if tenantId-schema accessed without valid context

### Admin Bypass

System admins (`RoleEnum.ADMIN`) skip membership validation by default.
Disable: `multiTenancy: { adminBypass: false }`.

## Compatibility Notes

| Pattern | Status | Notes |
|---------|--------|-------|
| `multiTenancy: {}` | Compatible | Enables tenant system with defaults |
| `multiTenancy: { enabled: false }` | Compatible | Pre-configured but disabled |
| No `multiTenancy` config | Compatible | Zero overhead, no changes needed |
| `excludeSchemas` | Compatible | Works as before; `TenantMember` auto-added |
| `RequestContext.runWithBypassTenantGuard()` | Compatible | Unchanged |
| Public endpoints on non-tenant schemas | Compatible | Unaffected by Safety Net |
| Public endpoints on tenant schemas | **Breaking** | Safety Net throws 403 — use `@SkipTenantCheck()` + bypass |

## Troubleshooting

| Issue | Cause | Fix |
|-------|-------|-----|
| 403 "Authentication required for tenant access" | Header present but no user | Authenticate before accessing tenant endpoints |
| 403 "Insufficient tenant role" | Membership role too low for hierarchy requirement | Upgrade membership role or use a lower-level decorator |
| 403 "Insufficient role" | No header, user.roles don't meet requirement | Add appropriate role to user.roles or send tenant header |
| 403 "Not a member of this tenant" | User has no active membership | Add membership via `CoreTenantService.addMember()` |
| 403 "Tenant context required" | Safety Net: tenantId-schema accessed without valid context | Add `@SkipTenantCheck()` + `runWithBypassTenantGuard()`, or provide tenant header |
| Data not filtered by tenant | Model missing `tenantId` field | Add `@Prop({ type: String }) tenantId: string` |
| TypeScript error: `userField` not found | Breaking change in IMultiTenancy | Remove `userField` from config |

## Rollback / Downgrade

If you need to revert to 11.20.x after upgrading:

1. **Downgrade the package:**
   ```bash
   pnpm add @lenne.tech/nest-server@11.20.1
   ```

2. **Restore `userField` in config:**
   ```typescript
   multiTenancy: {
     userField: 'tenantId', // Re-add the removed property
     excludeSchemas: ['User'],
   }
   ```

3. **Remove tenant decorators** from any endpoints where you added `@Roles(DefaultHR.*)`, `@SkipTenantCheck()`, or `@CurrentTenant()`.

4. **Remove `X-Tenant-Id` header** from frontend API calls if already added.

5. **Note:** The `TenantMember` collection created by 11.21.0 will remain in MongoDB but is unused by 11.20.x. You can drop it manually if desired:
   ```
   db.tenantmembers.drop()
   ```

## Module Documentation

- [Tenant Module README](../src/core/modules/tenant/README.md)
- [Integration Checklist](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
