@flusys/nestjs-auth 4.0.2 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,96 @@
1
- # Authentication Package Guide
1
+ # @flusys/nestjs-auth
2
2
 
3
- > **Package:** `@flusys/nestjs-auth`
4
- > **Version:** 4.0.2
5
- > **Type:** Authentication system with JWT, email verification, multi-tenant, and company/branch support
3
+ > Production-grade authentication and user management module for NestJS — with JWT, email verification, multi-company hierarchy, multi-tenant support, and pluggable extensibility.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-auth.svg)](https://www.npmjs.com/package/@flusys/nestjs-auth)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![NestJS](https://img.shields.io/badge/NestJS-11.x-red.svg)](https://nestjs.com/)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.x-green.svg)](https://nodejs.org/)
10
+
11
+ ---
6
12
 
7
13
  ## Table of Contents
8
14
 
15
+ - [Overview](#overview)
16
+ - [Features](#features)
17
+ - [Compatibility](#compatibility)
9
18
  - [Installation](#installation)
10
19
  - [Quick Start](#quick-start)
11
- - [Configuration](#configuration)
12
- - [Constants](#constants)
13
- - [Entities](#entities)
14
- - [Provider Interfaces](#provider-interfaces)
15
- - [Services](#services)
20
+ - [Module Registration](#module-registration)
21
+ - [forRoot (Sync)](#forroot-sync)
22
+ - [forRootAsync (Factory)](#forrootasync-factory)
23
+ - [forRootAsync (Class)](#forrootasync-class)
24
+ - [Configuration Reference](#configuration-reference)
25
+ - [Bootstrap Config](#bootstrap-config)
26
+ - [Runtime Config](#runtime-config)
27
+ - [Feature Toggles](#feature-toggles)
28
+ - [Deployment Modes](#deployment-modes)
16
29
  - [API Endpoints](#api-endpoints)
17
- - [Login Flows](#login-flows)
18
- - [Email Verification Flow](#email-verification-flow)
19
- - [User Management](#user-management)
20
- - [No-IAM Mode](#no-iam-mode)
21
- - [JWT Strategy](#jwt-strategy)
30
+ - [Entities](#entities)
31
+ - [Provider Interfaces (Extensibility)](#provider-interfaces-extensibility)
32
+ - [AUTH_EMAIL_PROVIDER](#auth_email_provider)
33
+ - [USER_ENRICHER](#user_enricher)
34
+ - [Exported Services](#exported-services)
22
35
  - [Interceptors](#interceptors)
23
- - [Validators](#validators)
24
- - [API Reference](#api-reference)
36
+ - [Security](#security)
37
+ - [Environment Variables](#environment-variables)
38
+ - [Troubleshooting](#troubleshooting)
39
+ - [License](#license)
40
+
41
+ ---
42
+
43
+ ## Overview
44
+
45
+ `@flusys/nestjs-auth` is a complete, drop-in authentication system for NestJS applications. It supports three deployment topologies out of the box:
46
+
47
+ | Mode | Description |
48
+ |------|-------------|
49
+ | **Single** | One database, one tenant, no company hierarchy |
50
+ | **Multi-Company** | One database, users belong to multiple companies/branches |
51
+ | **Multi-Tenant** | Separate database per tenant, full data isolation |
52
+
53
+ All features are opt-in via bootstrap configuration — enabling or disabling them at startup time.
54
+
55
+ ---
56
+
57
+ ## Features
58
+
59
+ - **JWT Authentication** — Access token + refresh token pair with configurable expiration
60
+ - **HTTP-only Cookie** — Refresh token stored in a secure, HTTP-only cookie for browser clients; body-based for mobile/API clients
61
+ - **Email Verification** — Registration-gated email verification flow with token-based links
62
+ - **Password Security** — bcrypt hashing with 12 salt rounds; SHA-256 hashing for stored tokens
63
+ - **Rate Limiting** — Built-in throttle limits on login (5/min), register (3/min), and refresh (30/min)
64
+ - **Company / Branch Hierarchy** — Optional multi-company support with branch sub-groups
65
+ - **Company Selection Flow** — Session-based company selection when a user belongs to multiple companies
66
+ - **User Management CRUD** — Full user administration endpoints with permission guards
67
+ - **Profile Enrichment** — Pluggable `USER_ENRICHER` for adding roles, permissions, and multi-step profile sections
68
+ - **Email Integration** — Pluggable `AUTH_EMAIL_PROVIDER` to send password-reset and verification emails via any provider
69
+ - **Multi-Tenant Support** — Dynamic DataSource resolution per request for multiple databases
70
+ - **Swagger Documentation** — All endpoints documented with `@nestjs/swagger`
71
+ - **Passport.js + JWT Strategy** — Standard NestJS Passport integration
72
+
73
+ ---
74
+
75
+ ## Compatibility
76
+
77
+ | Package | Version |
78
+ |---------|---------|
79
+ | `@nestjs/core` | `^11.0.0` |
80
+ | `@nestjs/common` | `^11.0.0` |
81
+ | `@nestjs/jwt` | `^10.0.0` |
82
+ | `@nestjs/passport` | `^10.0.0` |
83
+ | `typeorm` | `^0.3.0` |
84
+ | `passport-jwt` | `^4.0.0` |
85
+ | `bcrypt` | `^5.0.0` |
86
+ | Node.js | `>= 18.x` |
87
+ | TypeScript | `>= 5.0` |
88
+
89
+ **Peer dependencies (must be installed in your app):**
90
+
91
+ ```
92
+ @flusys/nestjs-core @flusys/nestjs-shared
93
+ ```
25
94
 
26
95
  ---
27
96
 
@@ -31,9 +100,21 @@
31
100
  npm install @flusys/nestjs-auth @flusys/nestjs-shared @flusys/nestjs-core
32
101
  ```
33
102
 
103
+ ```bash
104
+ # yarn
105
+ yarn add @flusys/nestjs-auth @flusys/nestjs-shared @flusys/nestjs-core
106
+ ```
107
+
108
+ ```bash
109
+ # pnpm
110
+ pnpm add @flusys/nestjs-auth @flusys/nestjs-shared @flusys/nestjs-core
111
+ ```
112
+
113
+ ---
114
+
34
115
  ## Quick Start
35
116
 
36
- ### Basic Setup (Single Database, No Company Feature)
117
+ Register `AuthModule` in your root `AppModule`. This minimal setup enables JWT authentication, user CRUD, and email verification in a single-database application:
37
118
 
38
119
  ```typescript
39
120
  import { Module } from '@nestjs/common';
@@ -47,18 +128,22 @@ import { AuthModule } from '@flusys/nestjs-auth';
47
128
  bootstrapAppConfig: {
48
129
  databaseMode: 'single',
49
130
  enableCompanyFeature: false,
131
+ enableEmailVerification: true,
50
132
  },
51
133
  config: {
52
134
  defaultDatabaseConfig: {
53
135
  type: 'postgres',
54
- host: 'localhost',
55
- port: 5432,
56
- username: 'postgres',
57
- password: 'password',
58
- database: 'myapp',
136
+ host: process.env.DB_HOST,
137
+ port: Number(process.env.DB_PORT ?? 5432),
138
+ username: process.env.DB_USER,
139
+ password: process.env.DB_PASSWORD,
140
+ database: process.env.DB_NAME,
59
141
  },
60
142
  jwtSecret: process.env.JWT_SECRET,
61
- jwtExpiration: '1h',
143
+ jwtExpiration: '15m',
144
+ refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET,
145
+ refreshTokenExpiration: '7d',
146
+ frontendUrl: process.env.FRONTEND_URL,
62
147
  },
63
148
  }),
64
149
  ],
@@ -66,7 +151,13 @@ import { AuthModule } from '@flusys/nestjs-auth';
66
151
  export class AppModule {}
67
152
  ```
68
153
 
69
- ### With Company Feature
154
+ ---
155
+
156
+ ## Module Registration
157
+
158
+ ### forRoot (Sync)
159
+
160
+ Use when all configuration values are available synchronously (environment variables, hardcoded values):
70
161
 
71
162
  ```typescript
72
163
  AuthModule.forRoot({
@@ -74,355 +165,371 @@ AuthModule.forRoot({
74
165
  includeController: true,
75
166
  bootstrapAppConfig: {
76
167
  databaseMode: 'single',
77
- enableCompanyFeature: true,
168
+ enableCompanyFeature: false,
169
+ enableEmailVerification: true,
78
170
  },
79
171
  config: {
80
- defaultDatabaseConfig: { /* ... */ },
172
+ defaultDatabaseConfig: { /* TypeORM DataSourceOptions */ },
81
173
  jwtSecret: process.env.JWT_SECRET,
174
+ jwtExpiration: '15m',
82
175
  refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET,
83
176
  refreshTokenExpiration: '7d',
84
- refreshTokenCookieName: 'fsn_refresh_token',
177
+ frontendUrl: 'http://localhost:3000',
85
178
  },
179
+ providers: [], // Optional: AUTH_EMAIL_PROVIDER, USER_ENRICHER, etc.
86
180
  })
87
181
  ```
88
182
 
89
- ### Multi-Tenant Mode
183
+ ### forRootAsync (Factory)
184
+
185
+ Use when configuration is loaded from an external service or config module:
90
186
 
91
187
  ```typescript
92
- AuthModule.forRoot({
188
+ import { ConfigService } from '@nestjs/config';
189
+ import { AuthModule } from '@flusys/nestjs-auth';
190
+
191
+ AuthModule.forRootAsync({
93
192
  global: true,
94
193
  includeController: true,
194
+ imports: [ConfigModule],
95
195
  bootstrapAppConfig: {
96
- databaseMode: 'multi-tenant',
196
+ databaseMode: 'single',
97
197
  enableCompanyFeature: true,
198
+ enableEmailVerification: true,
98
199
  },
99
- config: {
100
- tenantDefaultDatabaseConfig: {
200
+ useFactory: (configService: ConfigService) => ({
201
+ defaultDatabaseConfig: {
101
202
  type: 'postgres',
102
- host: 'localhost',
103
- port: 5432,
104
- username: 'postgres',
105
- password: 'password',
203
+ host: configService.get('DB_HOST'),
204
+ port: configService.get<number>('DB_PORT'),
205
+ username: configService.get('DB_USER'),
206
+ password: configService.get('DB_PASSWORD'),
207
+ database: configService.get('DB_NAME'),
106
208
  },
107
- tenants: [
108
- { id: 'tenant1', database: 'tenant1_db', name: 'Tenant 1', isActive: true },
109
- { id: 'tenant2', database: 'tenant2_db', name: 'Tenant 2', isActive: true },
110
- ],
111
- jwtSecret: process.env.JWT_SECRET,
112
- },
209
+ jwtSecret: configService.get('JWT_SECRET'),
210
+ jwtExpiration: configService.get('JWT_EXPIRATION', '15m'),
211
+ refreshTokenSecret: configService.get('REFRESH_TOKEN_SECRET'),
212
+ refreshTokenExpiration: configService.get('REFRESH_TOKEN_EXPIRATION', '7d'),
213
+ frontendUrl: configService.get('FRONTEND_URL'),
214
+ }),
215
+ inject: [ConfigService],
216
+ providers: [emailProviderFactory, userEnricherProvider],
113
217
  })
114
218
  ```
115
219
 
116
- ### With Email Provider (Password Reset, Email Verification)
220
+ ### forRootAsync (Class)
221
+
222
+ Use when configuration is encapsulated in a dedicated service:
117
223
 
118
224
  ```typescript
119
- import { AUTH_EMAIL_PROVIDER } from '@flusys/nestjs-auth';
120
- import { EmailSendService } from '@flusys/nestjs-email';
225
+ import { AuthOptionsFactory, IAuthModuleConfig } from '@flusys/nestjs-auth';
226
+ import { Injectable } from '@nestjs/common';
121
227
 
122
- AuthModule.forRoot({
123
- // ... config
124
- providers: [
125
- {
126
- provide: AUTH_EMAIL_PROVIDER,
127
- useFactory: (emailService: EmailSendService) => ({
128
- sendPasswordResetEmail: async (email, token, resetUrl) => {
129
- await emailService.sendTemplateEmail({
130
- templateSlug: 'password-reset',
131
- to: email,
132
- variables: { resetUrl, token },
133
- });
134
- },
135
- sendVerificationEmail: async (email, token, verifyUrl) => {
136
- await emailService.sendTemplateEmail({
137
- templateSlug: 'email-verification',
138
- to: email,
139
- variables: { verifyUrl, token },
140
- });
141
- },
142
- }),
143
- inject: [EmailSendService],
144
- },
145
- ],
228
+ @Injectable()
229
+ export class MyAuthConfigFactory implements AuthOptionsFactory {
230
+ createAuthOptions(): IAuthModuleConfig {
231
+ return {
232
+ jwtSecret: process.env.JWT_SECRET,
233
+ jwtExpiration: '15m',
234
+ refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET,
235
+ refreshTokenExpiration: '7d',
236
+ };
237
+ }
238
+ createOptions() { return this.createAuthOptions(); }
239
+ }
240
+
241
+ // In module:
242
+ AuthModule.forRootAsync({
243
+ bootstrapAppConfig: { databaseMode: 'single', enableCompanyFeature: false },
244
+ useClass: MyAuthConfigFactory,
146
245
  })
147
246
  ```
148
247
 
149
- ## Configuration
248
+ ---
249
+
250
+ ## Configuration Reference
251
+
252
+ ### Bootstrap Config
150
253
 
151
- ### Configuration Options
254
+ Bootstrap configuration is **fixed at startup** — it determines which entities, controllers, and services are registered. It cannot change at runtime.
152
255
 
153
256
  ```typescript
154
- interface AuthModuleOptions {
155
- global?: boolean; // Make module global
156
- includeController?: boolean; // Include default controllers
157
- bootstrapAppConfig?: {
158
- databaseMode: 'single' | 'multi-tenant';
159
- enableCompanyFeature: boolean;
160
- enableEmailVerification?: boolean; // Default: true
161
- };
162
- config?: {
163
- defaultDatabaseConfig?: IDatabaseConfig;
164
- tenantDefaultDatabaseConfig?: IDatabaseConfig;
165
- tenants?: ITenantDatabaseConfig[];
166
- jwtSecret?: string;
167
- jwtExpiration?: string; // Default: '1h'
168
- refreshTokenSecret?: string;
169
- refreshTokenExpiration?: string; // Default: '7d'
170
- refreshTokenCookieName?: string; // Default: 'fsn_refresh_token'
171
- frontendUrl?: string; // For email links
172
- };
173
- providers?: Provider[]; // Additional providers (AUTH_EMAIL_PROVIDER, USER_ENRICHER)
257
+ interface IBootstrapAppConfig {
258
+ /** Database topology */
259
+ databaseMode: 'single' | 'multi-tenant';
260
+
261
+ /** Enable company/branch hierarchy and related controllers */
262
+ enableCompanyFeature: boolean;
263
+
264
+ /** Enable email verification flow (default: true) */
265
+ enableEmailVerification?: boolean;
174
266
  }
175
267
  ```
176
268
 
177
- **Bootstrap Configuration Flags:**
269
+ ### Runtime Config
178
270
 
179
- | Flag | Type | Default | Effect |
180
- |------|------|---------|--------|
181
- | `enableCompanyFeature` | `boolean` | `false` | Enables company/branch entities and APIs |
182
- | `enableEmailVerification` | `boolean` | `true` | When `false`: excludes `AuthEmailService`, hides email-related APIs (forgot-password, reset-password, verify-email, resend-verification), and excludes `PendingRegistration` entity from migrations |
183
-
184
- ## Constants
271
+ Runtime configuration is loaded at startup but can be sourced from environment variables, config files, or external APIs.
185
272
 
186
273
  ```typescript
187
- // Injection Tokens
188
- export const AUTH_MODULE_OPTIONS = 'AUTH_MODULE_OPTIONS';
189
- export const USER_ENRICHER = 'USER_ENRICHER';
274
+ interface IAuthModuleConfig {
275
+ /** TypeORM DataSource options for the default (or single) database */
276
+ defaultDatabaseConfig?: DataSourceOptions;
190
277
 
191
- // Security
192
- export const BCRYPT_SALT_ROUNDS = 12;
278
+ /** Tenant database configs (multi-tenant mode only) */
279
+ tenants?: Array<{ id: string; databaseConfig: DataSourceOptions }>;
280
+
281
+ /** JWT access token secret */
282
+ jwtSecret?: string;
193
283
 
194
- // Cookie
195
- export const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
196
- export const MAX_COOKIE_BYTES = 3500;
284
+ /** JWT access token expiration (e.g. '15m', '1h', '7d') */
285
+ jwtExpiration?: string;
197
286
 
198
- // Cache Key Prefixes
199
- export const SESSION_CACHE_PREFIX = 'auth:session';
200
- export const TOKEN_REVOKED_PREFIX = 'auth:revoked';
287
+ /** JWT refresh token secret */
288
+ refreshTokenSecret?: string;
289
+
290
+ /** Refresh token expiration (e.g. '7d', '30d') */
291
+ refreshTokenExpiration?: string;
292
+
293
+ /** HTTP-only cookie name for refresh token (default: 'fsn_refresh_token') */
294
+ refreshTokenCookieName?: string;
295
+
296
+ /** Frontend URL used to build email verification and password reset links */
297
+ frontendUrl?: string;
298
+ }
201
299
  ```
202
300
 
203
- ## Entities
301
+ ---
204
302
 
205
- ### Entity Groups
303
+ ## Feature Toggles
206
304
 
207
- The auth module organizes entities into groups based on feature flags:
305
+ All features are independently gated by `bootstrapAppConfig`:
306
+
307
+ | Feature | Config Key | Default | Effect when enabled |
308
+ |---------|-----------|---------|---------------------|
309
+ | Company / Branch | `enableCompanyFeature: true` | `false` | Registers `Company`, `CompanyBranch`, `UserCompanyPermission` entities; adds company, branch, company-selection, and user-permission controllers |
310
+ | Email Verification | `enableEmailVerification: true` | `true` | Registers `PendingRegistration` entity; adds email-verification controller; blocks login until email is confirmed |
311
+
312
+ ---
313
+
314
+ ## Deployment Modes
315
+
316
+ ### Single Database (No Company)
208
317
 
209
318
  ```typescript
210
- // Internal entity groups (not exported directly)
211
- // Base entities: User, UserToken
212
- // Email entities: PendingRegistration
213
- // Company entities: Company, CompanyBranch, UserCompanyPermission
214
-
215
- // Exported interface for configuration
216
- export interface IAuthEntitiesConfig {
217
- enableCompanyFeature?: boolean;
218
- enableEmailVerification?: boolean; // Default: true
319
+ bootstrapAppConfig: {
320
+ databaseMode: 'single',
321
+ enableCompanyFeature: false,
322
+ enableEmailVerification: true,
219
323
  }
324
+ ```
220
325
 
221
- // Get entities based on feature flags (exported)
222
- export function getEntitiesByConfig(config: IAuthEntitiesConfig): any[] {
223
- const { enableCompanyFeature = false, enableEmailVerification = true } = config;
326
+ ### Single Database + Multi-Company
224
327
 
225
- const entities = [User, UserToken];
226
- if (enableEmailVerification) entities.push(PendingRegistration);
227
- if (enableCompanyFeature) entities.push(Company, CompanyBranch, UserCompanyPermission);
228
- return entities;
328
+ ```typescript
329
+ bootstrapAppConfig: {
330
+ databaseMode: 'single',
331
+ enableCompanyFeature: true,
332
+ enableEmailVerification: true,
229
333
  }
230
334
  ```
231
335
 
232
- **Usage:**
233
- ```typescript
234
- import { getEntitiesByConfig } from '@flusys/nestjs-auth/entities';
336
+ When a user belongs to multiple companies, login returns a `CompanySelectionResponseDto` instead of tokens. The client must call `/auth/select-company` with a `sessionId` to complete login.
235
337
 
236
- // Get entities for single database without company feature
237
- const entities = getEntitiesByConfig({ enableCompanyFeature: false });
238
- // Returns: [User, UserToken, PendingRegistration]
338
+ ### Multi-Tenant (Separate DB per Tenant)
239
339
 
240
- // Get entities with company feature, no email verification
241
- const entities = getEntitiesByConfig({ enableCompanyFeature: true, enableEmailVerification: false });
242
- // Returns: [User, UserToken, Company, CompanyBranch, UserCompanyPermission]
340
+ ```typescript
341
+ bootstrapAppConfig: {
342
+ databaseMode: 'multi-tenant',
343
+ enableCompanyFeature: true,
344
+ enableEmailVerification: true,
345
+ },
346
+ config: {
347
+ defaultDatabaseConfig: { /* fallback/core DB */ },
348
+ tenants: [
349
+ { id: 'tenant-a', databaseConfig: { /* ... */ } },
350
+ { id: 'tenant-b', databaseConfig: { /* ... */ } },
351
+ ],
352
+ jwtSecret: process.env.JWT_SECRET,
353
+ // ...
354
+ }
243
355
  ```
244
356
 
245
- **Entity Selection by Feature Flags:**
357
+ ---
246
358
 
247
- | enableEmailVerification | enableCompanyFeature | Entities |
248
- |------------------------|---------------------|----------|
249
- | `true` (default) | `false` | User, UserToken, PendingRegistration |
250
- | `true` | `true` | All entities |
251
- | `false` | `false` | User, UserToken only |
252
- | `false` | `true` | User, UserToken, Company, CompanyBranch, UserCompanyPermission |
359
+ ## API Endpoints
253
360
 
254
- ### User Entity
361
+ All endpoints use **POST** (POST-only RPC design). Authentication required unless marked **Public**.
255
362
 
256
- Extends `UserRoot` from `@flusys/nestjs-shared`:
363
+ ### Authentication — `POST /auth/*`
257
364
 
258
- | Field | Type | Description |
259
- |-------|------|-------------|
260
- | `id` | `uuid` | Primary key |
261
- | `name` | `varchar(255)` | User's name |
262
- | `email` | `varchar(255)` | Unique email address |
263
- | `password` | `text` | Bcrypt hashed password |
264
- | `phone` | `varchar(255)` | Phone number |
265
- | `isActive` | `boolean` | Account status |
266
- | `emailVerified` | `boolean` | Email verification status |
267
- | `phoneVerified` | `boolean` | Phone verification status |
268
- | `profilePictureId` | `uuid` | Reference to storage file |
269
- | `lastLoginAt` | `timestamp` | Last login timestamp |
270
- | `additionalFields` | `json` | Extensible JSON fields |
271
- | `createdAt` | `timestamp` | Auto-generated |
272
- | `updatedAt` | `timestamp` | Auto-updated |
273
- | `deletedAt` | `timestamp` | Soft delete timestamp |
365
+ | Endpoint | Auth | Rate Limit | Description |
366
+ |----------|------|-----------|-------------|
367
+ | `POST /auth/login` | Public | 5/min | Login with email + password |
368
+ | `POST /auth/register` | Public | 3/min | Register a new user account |
369
+ | `POST /auth/refresh` | Public | 30/min | Refresh access token (cookie or body) |
370
+ | `POST /auth/me` | JWT | — | Get current user info |
371
+ | `POST /auth/logout` | JWT | — | Logout and clear refresh token cookie |
372
+ | `POST /auth/change-password` | JWT | 5/min | Change authenticated user's password |
274
373
 
275
- ### UserToken Entity
374
+ #### Login Response Variants
276
375
 
277
- Stores tokens for existing user actions:
376
+ Login returns one of three shapes depending on configuration:
278
377
 
279
378
  ```typescript
280
- enum TokenType {
281
- PASSWORD_RESET = 'password_reset',
282
- EMAIL_VERIFICATION = 'email_verification',
283
- EMAIL_CHANGE = 'email_change',
284
- }
379
+ // 1. Standard login success
380
+ { accessToken: string; refreshToken: string; user: IUser }
381
+
382
+ // 2. Company selection required (enableCompanyFeature: true, multiple companies)
383
+ { requiresSelection: true; sessionId: string; expiresAt: string; user: IUser; companies: ICompany[] }
384
+
385
+ // 3. Email verification required (enableEmailVerification: true, unverified account)
386
+ { requiresEmailVerification: true; email: string; message: string }
387
+ ```
388
+
389
+ #### Refresh Token — Browser vs Mobile
390
+
391
+ ```
392
+ Browser clients: Refresh token read from HTTP-only cookie (set automatically on login).
393
+ Falls back to request body if cookie is absent.
394
+
395
+ Mobile / API: Send refreshToken in request body.
396
+ Set header: x-client-type: mobile OR x-client-type: api
285
397
  ```
286
398
 
287
- | Field | Type | Description |
288
- |-------|------|-------------|
289
- | `id` | `uuid` | Primary key |
290
- | `userId` | `uuid` | Reference to user |
291
- | `type` | `TokenType` | Token purpose |
292
- | `token` | `varchar(64)` | SHA-256 hashed token |
293
- | `expiresAt` | `timestamp` | Expiration time |
294
- | `usedAt` | `timestamp` | When token was used |
295
- | `metadata` | `varchar(255)` | Additional data (e.g., new email for EMAIL_CHANGE) |
296
- | `createdAt` | `timestamp` | Auto-generated |
297
-
298
- **Indexes:**
299
- - `IDX_user_token_token` - Unique on token
300
- - `IDX_user_token_user_type` - userId + type
301
- - `IDX_user_token_expires` - For cleanup queries
302
-
303
- ### PendingRegistration Entity
304
-
305
- Stores registration data until email verification (prevents garbage accounts):
306
-
307
- | Field | Type | Description |
308
- |-------|------|-------------|
309
- | `id` | `uuid` | Primary key |
310
- | `email` | `varchar(255)` | Unique pending email |
311
- | `name` | `varchar(255)` | User's name |
312
- | `phone` | `varchar(255)` | Phone number |
313
- | `passwordHash` | `text` | Pre-hashed password |
314
- | `token` | `varchar(64)` | SHA-256 hashed verification token |
315
- | `expiresAt` | `timestamp` | Expiration (24 hours) |
316
- | `companyData` | `json` | Company registration data |
317
- | `additionalFields` | `json` | Extension fields for USER_ENRICHER |
318
- | `createdAt` | `timestamp` | Auto-generated |
319
-
320
- **Indexes:**
321
- - `IDX_pending_reg_email` - Unique
322
- - `IDX_pending_reg_token` - Unique
323
- - `IDX_pending_reg_expires` - For cleanup
324
-
325
- ### Company Entity (when company feature enabled)
326
-
327
- | Field | Type | Description |
328
- |-------|------|-------------|
329
- | `id` | `uuid` | Primary key |
330
- | `readOnly` | `boolean` | Read-only flag (default: false) |
331
- | `name` | `varchar(255)` | Company name (unique) |
332
- | `slug` | `varchar(255)` | URL-friendly identifier (unique) |
333
- | `logoId` | `char(36)` | Reference to storage file |
334
- | `address` | `text` | Company address |
335
- | `phone` | `varchar(50)` | Contact phone |
336
- | `email` | `varchar(255)` | Contact email |
337
- | `website` | `varchar(100)` | Company website URL |
338
- | `serial` | `int` | Display order |
339
- | `isActive` | `boolean` | Company status (default: true) |
340
- | `settings` | `json` | Company-specific settings |
341
- | `additionalFields` | `json` | Extension fields |
342
- | `createdAt` | `timestamp` | Auto-generated |
343
- | `updatedAt` | `timestamp` | Auto-updated |
344
-
345
- **Relations:**
346
- - `branches` - One-to-Many relation to CompanyBranch
347
-
348
- ### CompanyBranch Entity
349
-
350
- | Field | Type | Description |
351
- |-------|------|-------------|
352
- | `id` | `uuid` | Primary key |
353
- | `readOnly` | `boolean` | Read-only flag (default: false) |
354
- | `name` | `varchar(255)` | Branch name |
355
- | `slug` | `varchar(255)` | URL-friendly identifier (unique per company) |
356
- | `logoId` | `char(36)` | Reference to storage file |
357
- | `address` | `text` | Branch address |
358
- | `phone` | `varchar(50)` | Branch phone |
359
- | `email` | `varchar(255)` | Branch email |
360
- | `serial` | `int` | Display order |
361
- | `isActive` | `boolean` | Branch status (default: true) |
362
- | `additionalFields` | `json` | Extension fields |
363
- | `parentId` | `uuid` | Parent branch (for hierarchical branches) |
364
- | `companyId` | `uuid` | Parent company ID |
365
- | `createdAt` | `timestamp` | Auto-generated |
366
- | `updatedAt` | `timestamp` | Auto-updated |
367
-
368
- **Relations:**
369
- - `company` - Many-to-One relation to Company (CASCADE delete)
370
- - `parent` - Many-to-One relation to self (hierarchical, CASCADE delete)
371
- - `children` - One-to-Many relation to self
372
-
373
- ### UserCompanyPermission Entity (Polymorphic)
399
+ ### Email Verification — `POST /auth/email-verification/*`
400
+
401
+ Registered when `enableEmailVerification: true`.
402
+
403
+ | Endpoint | Auth | Description |
404
+ |----------|------|-------------|
405
+ | `POST /auth/email-verification/verify` | Public | Verify email with token from link |
406
+ | `POST /auth/email-verification/resend` | Public | Resend verification email |
407
+
408
+ ### Company Selection — `POST /auth/company-selection/*`
409
+
410
+ Registered when `enableCompanyFeature: true`.
411
+
412
+ | Endpoint | Auth | Description |
413
+ |----------|------|-------------|
414
+ | `POST /auth/company-selection/select` | Public + Session | Complete login by selecting company/branch |
415
+
416
+ ### User Administration — `POST /administration/users/*`
417
+
418
+ | Endpoint | Permission | Description |
419
+ |----------|-----------|-------------|
420
+ | `POST /administration/users/insert` | `user.create` | Create a new user |
421
+ | `POST /administration/users/insert-many` | `user.create` | Bulk create users |
422
+ | `POST /administration/users/get-all` | `user.read` | Paginated user list |
423
+ | `POST /administration/users/get/:id` | `user.read` | Get user by ID |
424
+ | `POST /administration/users/update` | `user.update` | Update user fields |
425
+ | `POST /administration/users/update-many` | `user.update` | Bulk update users |
426
+ | `POST /administration/users/delete` | `user.delete` | Soft delete user |
427
+ | `POST /administration/users/profile` | JWT | Update own profile |
428
+ | `POST /administration/users/profile-sections` | `user.read` | List profile section definitions |
429
+ | `POST /administration/users/:id/profile-extras` | `user.read` | Get user with enriched extras |
430
+ | `POST /administration/users/:id/profile-section/:sectionId` | `user.read` | Get profile section data |
431
+ | `POST /administration/users/:id/profile-section/:sectionId/update` | `user.update` | Update profile section |
432
+ | `POST /administration/users/:id/profile-completion` | `user.read` | Profile completion percentage |
433
+ | `POST /administration/users/:id/verify-email` | `user.update` | Admin: mark email as verified |
434
+ | `POST /administration/users/:id/verify-phone` | `user.update` | Admin: mark phone as verified |
435
+ | `POST /administration/users/:id/status` | `user.update` | Enable / disable user |
436
+
437
+ ### Company & Branch — `POST /companies/*`, `POST /branches/*`
438
+
439
+ Registered when `enableCompanyFeature: true`.
440
+
441
+ | Endpoint | Permission | Description |
442
+ |----------|-----------|-------------|
443
+ | `POST /companies/insert` | `company.create` | Create company |
444
+ | `POST /companies/get-all` | `company.read` | List companies |
445
+ | `POST /companies/get/:id` | `company.read` | Get company by ID |
446
+ | `POST /companies/update` | `company.update` | Update company |
447
+ | `POST /companies/delete` | `company.delete` | Delete company |
448
+ | `POST /branches/insert` | `branch.create` | Create branch |
449
+ | `POST /branches/get-all` | `branch.read` | List branches |
450
+ | `POST /branches/update` | `branch.update` | Update branch |
451
+ | `POST /branches/delete` | `branch.delete` | Delete branch |
452
+
453
+ ### User Permissions — `POST /administration/user-permissions/*`
454
+
455
+ Registered when `enableCompanyFeature: true`.
456
+
457
+ | Endpoint | Permission | Description |
458
+ |----------|-----------|-------------|
459
+ | `POST /administration/user-permissions/assign` | `user.update` | Assign user to company/branch |
460
+ | `POST /administration/user-permissions/remove` | `user.update` | Remove user from company/branch |
461
+ | `POST /administration/user-permissions/get-by-user` | `user.read` | Get all permissions for a user |
462
+
463
+ ---
464
+
465
+ ## Entities
466
+
467
+ ### Core Entities (always registered)
468
+
469
+ | Entity | Table | Description |
470
+ |--------|-------|-------------|
471
+ | `User` | `user` | User accounts. Passwords hashed with bcrypt (12 rounds) |
472
+ | `UserToken` | `user_token` | Password-reset and email-verification tokens (SHA-256 hashed at rest) |
473
+
474
+ ### Email Verification Entities (`enableEmailVerification: true`)
475
+
476
+ | Entity | Table | Description |
477
+ |--------|-------|-------------|
478
+ | `PendingRegistration` | `pending_registration` | Holds unverified registration data until email is confirmed |
479
+
480
+ ### Company Feature Entities (`enableCompanyFeature: true`)
481
+
482
+ | Entity | Table | Description |
483
+ |--------|-------|-------------|
484
+ | `Company` | `company` | Company with name, slug, logo, active flag |
485
+ | `CompanyBranch` | `company_branch` | Branch within a company |
486
+ | `UserCompanyPermission` | `user_company_permission` | User ↔ Company / Branch assignment with `PermissionType` |
487
+
488
+ #### Entity Registration in TypeORM
489
+
490
+ Use `AuthModule.getEntities()` to include auth entities in your `TypeOrmModule`:
374
491
 
375
492
  ```typescript
376
- enum PermissionType {
377
- COMPANY = 'company',
378
- BRANCH = 'branch',
379
- }
493
+ import { AuthModule } from '@flusys/nestjs-auth';
494
+
495
+ TypeOrmModule.forRoot({
496
+ // ...
497
+ entities: [
498
+ ...AuthModule.getEntities({ enableCompanyFeature: true, enableEmailVerification: true }),
499
+ // other app entities
500
+ ],
501
+ })
380
502
  ```
381
503
 
382
- | Field | Type | Description |
383
- |-------|------|-------------|
384
- | `id` | `uuid` | Primary key |
385
- | `userId` | `uuid` | Reference to user |
386
- | `permissionType` | `varchar(50)` | company or branch |
387
- | `targetId` | `uuid` | Company ID or Branch ID |
388
- | `metadata` | `json` | Optional metadata for the permission |
389
- | `isActive` | `boolean` | Permission status (default: true) |
390
- | `createdAt` | `timestamp` | Auto-generated |
391
- | `updatedAt` | `timestamp` | Auto-updated |
392
-
393
- **Indexes:**
394
- - Unique index on `(userId, permissionType, targetId)`
395
- - Index on `(userId, permissionType)`
396
- - Index on `(permissionType, targetId)`
397
-
398
- **Relations:**
399
- - `user` - Many-to-One relation to User (CASCADE delete)
400
- - `company` - Many-to-One relation to Company (no FK constraint - polymorphic)
401
- - `branch` - Many-to-One relation to CompanyBranch (no FK constraint - polymorphic)
402
-
403
- ## Provider Interfaces
504
+ ---
505
+
506
+ ## Provider Interfaces (Extensibility)
507
+
508
+ `@flusys/nestjs-auth` is designed to be standalone — it does not import `@flusys/nestjs-email` or `@flusys/nestjs-iam`. Optional features are connected via **provider interfaces** defined in this package and implemented in your application.
404
509
 
405
510
  ### AUTH_EMAIL_PROVIDER
406
511
 
407
- Allows auth to work independently of the email module:
512
+ Enables password-reset and email-verification emails without a hard dependency on any email module.
513
+
514
+ **Interface:**
408
515
 
409
516
  ```typescript
517
+ import { IAuthEmailProvider, AUTH_EMAIL_PROVIDER } from '@flusys/nestjs-auth';
518
+
410
519
  interface IAuthEmailProvider {
411
- /** Send password reset email */
412
520
  sendPasswordResetEmail(email: string, token: string, resetUrl: string): Promise<void>;
413
-
414
- /** Send email verification email */
415
521
  sendVerificationEmail(email: string, token: string, verifyUrl: string): Promise<void>;
416
-
417
- /** Send welcome email after registration (optional) */
418
- sendWelcomeEmail?(email: string, name: string): Promise<void>;
522
+ sendWelcomeEmail?(email: string, name: string): Promise<void>; // optional
419
523
  }
420
524
  ```
421
525
 
422
- **Example Implementation:**
526
+ **Implementation example (using `@flusys/nestjs-email`):**
423
527
 
424
528
  ```typescript
425
- {
529
+ import { AUTH_EMAIL_PROVIDER } from '@flusys/nestjs-auth';
530
+ import { EmailSendService } from '@flusys/nestjs-email';
531
+
532
+ export const authEmailProvider = {
426
533
  provide: AUTH_EMAIL_PROVIDER,
427
534
  useFactory: (emailSendService: EmailSendService) => ({
428
535
  sendPasswordResetEmail: async (email, token, resetUrl) => {
@@ -439,960 +546,295 @@ interface IAuthEmailProvider {
439
546
  variables: { verifyUrl, token },
440
547
  });
441
548
  },
549
+ sendWelcomeEmail: async (email, name) => {
550
+ await emailSendService.sendTemplateEmail({
551
+ templateSlug: 'welcome',
552
+ to: email,
553
+ variables: { name },
554
+ });
555
+ },
442
556
  }),
443
557
  inject: [EmailSendService],
444
- }
558
+ };
445
559
  ```
446
560
 
447
- ### USER_ENRICHER
448
-
449
- Extends user list and profile without modifying base package:
561
+ **Register in `forRootAsync`:**
450
562
 
451
563
  ```typescript
452
- interface IUserEnricher {
453
- // ===== List Enrichment =====
454
-
455
- /** Enrich user list query with extra joins/selects */
456
- enrichListQuery?(
457
- query: SelectQueryBuilder<User>,
458
- user: ILoggedUserInfo | null,
459
- ): Promise<{ extraFields: string[] }>;
460
-
461
- /** Transform user list items with extra computed data */
462
- enrichListItems?(
463
- users: IUser[],
464
- user: ILoggedUserInfo | null,
465
- ): Promise<IEnrichedUser[]>;
466
-
467
- // ===== Profile Enrichment =====
468
-
469
- /** Get extra profile data (roles, permissions, sections) */
470
- getProfileExtras?(
471
- userId: string,
472
- user: ILoggedUserInfo,
473
- ): Promise<IProfileExtras>;
474
-
475
- /** Handle profile update with extra fields (within transaction) */
476
- updateProfileExtras?(
477
- userId: string,
478
- extras: Record<string, any>,
479
- queryRunner: QueryRunner,
480
- ): Promise<void>;
481
-
482
- /** Validate extra fields before profile update */
483
- validateProfileExtras?(
484
- userId: string,
485
- extras: Record<string, any>,
486
- user: ILoggedUserInfo,
487
- ): Promise<void>;
488
-
489
- // ===== Multi-Step Profile Methods =====
490
-
491
- /** Get profile section definitions for multi-step profiles */
492
- getProfileSections?(): IProfileSection[];
493
-
494
- /** Get specific section data for a user */
495
- getProfileSectionData?(
496
- userId: string,
497
- sectionId: string,
498
- user: ILoggedUserInfo,
499
- ): Promise<IProfileSectionData>;
500
-
501
- /** Update specific profile section (within transaction) */
502
- updateProfileSection?(
503
- userId: string,
504
- sectionId: string,
505
- data: Record<string, any>,
506
- queryRunner: QueryRunner,
507
- ): Promise<void>;
508
-
509
- /** Handle file upload for profile section */
510
- handleSectionFileUpload?(
511
- userId: string,
512
- sectionId: string,
513
- field: string,
514
- fileInfo: { name: string; url: string; size: number; type: string },
515
- queryRunner: QueryRunner,
516
- ): Promise<IProfileFile>;
517
-
518
- /** Handle file deletion for profile section */
519
- handleSectionFileDelete?(
520
- userId: string,
521
- sectionId: string,
522
- field: string,
523
- fileId: string,
524
- queryRunner: QueryRunner,
525
- ): Promise<void>;
526
-
527
- /** Calculate profile completion percentage (0-100) */
528
- calculateProfileCompletion?(
529
- userId: string,
530
- user: ILoggedUserInfo,
531
- ): Promise<number>;
532
-
533
- // ===== Registration Hook =====
534
-
535
- /**
536
- * Hook called after user is created during registration
537
- * Runs within the same database transaction as user creation
538
- * Use this to create related entities (e.g., UserProfileInfo)
539
- * Throw error to rollback the entire registration
540
- */
541
- onUserCreated?(
542
- userId: string,
543
- additionalFields: Record<string, any> | null,
544
- queryRunner: QueryRunner,
545
- ): Promise<void>;
546
- }
564
+ AuthModule.forRootAsync({
565
+ // ...
566
+ providers: [authEmailProvider],
567
+ })
547
568
  ```
548
569
 
549
- **Profile Related Interfaces:**
570
+ When `AUTH_EMAIL_PROVIDER` is not provided, the auth module runs without email sending — registration still works but verification emails are silently skipped.
550
571
 
551
- ```typescript
552
- interface IProfileSection {
553
- id: string; // Unique identifier
554
- name: string; // Display name
555
- order: number; // Stepper order
556
- icon?: string; // PrimeNG icon class
557
- required?: boolean; // Required for completion
558
- hasFileUpload?: boolean; // Has file fields
559
- editPermission?: IPermissionLogic; // Required permission
560
- }
561
-
562
- interface IProfileSectionData {
563
- sectionId: string;
564
- data: Record<string, any>;
565
- isComplete: boolean;
566
- files?: IProfileFile[];
567
- }
568
-
569
- interface IProfileFile {
570
- field: string;
571
- id: string;
572
- name: string;
573
- url?: string;
574
- type?: string;
575
- }
576
-
577
- interface IProfileExtras {
578
- roles?: IProfileRole[];
579
- actions?: IProfileAction[];
580
- sections?: IProfileSectionData[];
581
- completionPercentage?: number;
582
- }
583
-
584
- interface IProfileRole {
585
- id: string;
586
- name: string;
587
- description?: string | null;
588
- }
572
+ ---
589
573
 
590
- interface IProfileAction {
591
- id: string;
592
- name: string;
593
- code: string;
594
- description?: string | null;
595
- }
574
+ ### USER_ENRICHER
596
575
 
597
- type IEnrichedUser = IUser & IProfileExtras;
598
- ```
576
+ Allows extending the user list and profile responses with data from external modules (roles, permissions, custom profile sections) without creating circular dependencies.
599
577
 
600
- **Registration Hook Example (UserProfileInfo):**
578
+ **Interface:**
601
579
 
602
580
  ```typescript
603
- // In your consuming app (e.g., FLUSYS-TEMPLATE)
604
- import { USER_ENRICHER, IUserEnricher } from '@flusys/nestjs-auth';
605
- import { QueryRunner } from 'typeorm';
606
-
607
- @Injectable({ scope: Scope.REQUEST })
608
- export class MyUserEnricher implements IUserEnricher {
609
- /**
610
- * Called after user creation, within the same transaction
611
- * If this throws, the entire registration rolls back
612
- */
613
- async onUserCreated(
614
- userId: string,
615
- additionalFields: Record<string, any> | null,
616
- queryRunner: QueryRunner,
617
- ): Promise<void> {
618
- if (!additionalFields) return;
619
-
620
- // Create related entity (e.g., UserProfileInfo)
621
- const profileInfo = queryRunner.manager.create(UserProfileInfo, {
622
- userId,
623
- prevCompanyName: additionalFields.prevCompanyName ?? null,
624
- prevCompanyLocation: additionalFields.prevCompanyLocation ?? null,
625
- });
626
-
627
- await queryRunner.manager.save(UserProfileInfo, profileInfo);
628
- }
629
- }
581
+ import { IUserEnricher, USER_ENRICHER } from '@flusys/nestjs-auth';
630
582
 
631
- // Register the provider
632
- {
633
- provide: USER_ENRICHER,
634
- useClass: MyUserEnricher,
635
- }
636
- ```
637
-
638
- **Registration with additionalFields:**
583
+ interface IUserEnricher {
584
+ // Extend the user list SQL query with extra joins/selects
585
+ enrichListQuery?(query: SelectQueryBuilder<User>, user: ILoggedUserInfo | null): Promise<{ extraFields: string[] }>;
639
586
 
640
- ```json
641
- {
642
- "name": "John Doe",
643
- "email": "john@example.com",
644
- "password": "password123",
645
- "additionalFields": {
646
- "prevCompanyName": "Acme Corp",
647
- "prevCompanyLocation": "New York"
648
- }
649
- }
650
- ```
587
+ // Transform user list items (add computed fields)
588
+ enrichListItems?(users: IUser[], user: ILoggedUserInfo | null): Promise<IEnrichedUser[]>;
651
589
 
652
- ## Services
653
-
654
- | Service | Scope | Description |
655
- |---------|-------|-------------|
656
- | `AuthenticationService` | REQUEST | Login, register, token management, company selection |
657
- | `AuthEmailService` | REQUEST | Email verification, password reset, pending registrations |
658
- | `UserService` | REQUEST | User CRUD, extends `RequestScopedApiService` |
659
- | `CompanyService` | REQUEST | Company CRUD (when company feature enabled) |
660
- | `BranchService` | REQUEST | Branch CRUD with company filtering |
661
- | `UserPermissionService` | REQUEST | User-company/branch permission management |
662
- | `AuthDataSourceProvider` | REQUEST | Dynamic datasource for single/multi-tenant |
663
- | `AuthConfigService` | SINGLETON | JWT and module configuration access |
664
- | `CompanySelectionSessionService` | SINGLETON | Session management for company selection |
665
-
666
- ### AuthEmailService
667
-
668
- Token Security: All tokens are stored as SHA-256 hashes. Raw tokens are sent to users via email, but only hashes are stored in the database.
669
-
670
- | Method | Description |
671
- |--------|-------------|
672
- | `isEmailEnabled()` | Check if email provider is configured |
673
- | `hasPendingRegistration(email)` | Check for pending registration |
674
- | `createPendingRegistration(data)` | Create pending registration with verification email |
675
- | `forgotPassword(email, resetUrlBase)` | Send password reset email |
676
- | `resetPassword(token, newPassword)` | Reset password using token |
677
- | `verifyEmail(token)` | Verify email (returns `{ type: 'pending', pending }` or `{ type: 'verified', message }`) |
678
- | `resendVerificationEmail(email, verifyUrlBase)` | Resend verification email |
679
- | `cleanupExpired()` | Clean up expired tokens and pending registrations |
680
-
681
- **Token Expiry:**
682
- - Password Reset: 1 hour
683
- - Email Verification: 24 hours
684
-
685
- ### AuthenticationService
686
-
687
- | Method | Description |
688
- |--------|-------------|
689
- | `login(email, password)` | Validate credentials and return tokens |
690
- | `register(dto, verifyUrlBase?)` | Register new user (creates pending registration if email enabled) |
691
- | `createUserFromPendingRegistration(pending)` | Create user from verified pending registration |
692
- | `select(sessionId, companyId, branchId)` | Complete company selection after login |
693
- | `switchCompany(userId, companyId, branchId)` | Switch to different company/branch |
694
- | `refreshToken(refreshToken)` | Refresh access token |
695
- | `changePassword(userId, currentPassword, newPassword)` | Change user password |
696
- | `logout(userId)` | Revoke all tokens for user |
697
- | `isTokenRevoked(userId, tokenIssuedAt)` | Check if token is revoked |
698
- | `getUserCompanies(userId)` | Get user's available companies |
699
- | `getUserBranches(userId, companyId)` | Get user's branches for a company |
700
- | `getUserInfo(userId, companyId?, branchId?)` | Get current user info |
701
-
702
- ### UserService
703
-
704
- Extends `RequestScopedApiService` for full CRUD support.
705
-
706
- | Method | Description |
707
- |--------|-------------|
708
- | `insert(dto, user)` | Create user (auto-assigns company permissions) |
709
- | `verifyEmail(id)` | Mark user email as verified |
710
- | `verifyPhone(id)` | Mark user phone as verified |
711
- | `updateStatus(id, isActive, user)` | Update user status (company-aware) |
712
- | `profile(dto, user)` | Update own profile with optional extras |
713
- | `getProfileWithExtras(userId, user)` | Get profile with enricher data |
714
- | `getProfileSections()` | Get profile section definitions |
715
- | `getProfileSectionData(userId, sectionId, user)` | Get specific section data |
716
- | `updateProfileSection(userId, sectionId, data, user)` | Update specific section |
717
- | `getProfileCompletion(userId, user)` | Calculate profile completion % |
718
- | `getEnrichedList(users, user)` | Enrich user list with extra data |
719
-
720
- ### CompanyService
721
-
722
- Extends `RequestScopedApiService` for full CRUD support on Company entity.
723
-
724
- ### BranchService
725
-
726
- Extends `RequestScopedApiService` for full CRUD support on CompanyBranch entity.
727
- Automatically applies company filter based on logged user context.
728
-
729
- ### UserPermissionService
730
-
731
- | Method | Description |
732
- |--------|-------------|
733
- | `findUserPermissions(userId, type)` | Get all permissions of type for user |
734
- | `assignPermission(userId, targetId, type, metadata?)` | Assign permission to user |
735
- | `revokePermission(userId, targetId, type)` | Revoke permission from user |
736
- | `findUserCompanies(userId)` | Get user's company permissions |
737
- | `findUserBranches(userId, companyId?)` | Get user's branch permissions |
738
-
739
- ### CompanySelectionSessionService
740
-
741
- Manages temporary sessions during company/branch selection flow.
742
-
743
- | Method | Description |
744
- |--------|-------------|
745
- | `createSession(userId)` | Create session (returns sessionId, TTL: 5 minutes) |
746
- | `getSession(sessionId)` | Get session without consuming |
747
- | `consumeSession(sessionId)` | Get and delete session (one-time use) |
748
- | `deleteSession(sessionId)` | Delete session |
749
-
750
- ### AuthConfigService
751
-
752
- Provides configuration access.
753
-
754
- | Method | Description |
755
- |--------|-------------|
756
- | `getDatabaseMode()` | Returns 'single' or 'multi-tenant' |
757
- | `isMultiTenant()` | Check if multi-tenant mode |
758
- | `isCompanyFeatureEnabled()` | Check if company feature enabled |
759
- | `isEmailVerificationEnabled()` | Check if email verification enabled |
760
- | `getJwtSecret()` | Get JWT secret (throws if not configured) |
761
- | `getJwtExpiration()` | Get JWT expiration (default: '1h') |
762
- | `getRefreshTokenSecret()` | Get refresh token secret |
763
- | `getRefreshTokenExpiration()` | Get refresh expiration (default: '7d') |
764
- | `getRefreshTokenCookieName()` | Get cookie name (default: 'fsn_refresh_token') |
765
- | `getFrontendUrl()` | Get frontend URL for email links |
766
-
767
- ### AuthDataSourceProvider
768
-
769
- Extends `MultiTenantDataSourceService` for dynamic datasource handling.
770
-
771
- | Method | Description |
772
- |--------|-------------|
773
- | `getDataSource()` | Get current tenant's DataSource |
774
- | `getRepository(entity)` | Get repository for entity |
775
- | `isCompanyFeatureEnabled(tenant?)` | Check company feature for tenant |
776
- | `isEmailVerificationEnabled(tenant?)` | Check email verification for tenant |
777
- | `getAuthEntities()` | Get entities based on feature flags |
590
+ // Return extra profile data (roles, actions, etc.)
591
+ getProfileExtras?(userId: string, user: ILoggedUserInfo): Promise<IProfileExtras>;
778
592
 
779
- ## API Endpoints
593
+ // Update extra profile fields inside a transaction
594
+ updateProfileExtras?(userId: string, extras: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
780
595
 
781
- > **Note:** All endpoints use POST method following POST-only RPC pattern.
782
-
783
- ### Authentication Endpoints
784
-
785
- | Endpoint | Method | Auth | Description |
786
- |----------|--------|------|-------------|
787
- | `/auth/login` | POST | No | Login with email/password |
788
- | `/auth/register` | POST | No | Register new user |
789
- | `/auth/refresh` | POST | No | Refresh access token |
790
- | `/auth/logout` | POST | JWT | Logout (revokes all tokens) |
791
- | `/auth/change-password` | POST | JWT | Change user password |
792
- | `/auth/me` | POST | JWT | Get current user info |
793
-
794
- **Rate Limits:**
795
- - `/auth/login` - 5 attempts per minute
796
- - `/auth/register` - 3 attempts per minute
797
- - `/auth/refresh` - 30 attempts per minute
798
-
799
- ### Email Authentication Endpoints
800
-
801
- | Endpoint | Method | Auth | Description |
802
- |----------|--------|------|-------------|
803
- | `/auth/forgot-password` | POST | No | Request password reset email |
804
- | `/auth/reset-password` | POST | No | Reset password using token |
805
- | `/auth/verify-email` | POST | No | Verify email using token |
806
- | `/auth/resend-verification` | POST | No | Resend email verification link |
807
-
808
- **Rate Limits:**
809
- - `/auth/forgot-password` - 3 requests per 5 minutes
810
- - `/auth/reset-password` - 5 attempts per 15 minutes
811
- - `/auth/verify-email` - 10 attempts per minute
812
- - `/auth/resend-verification` - 3 requests per 5 minutes
813
-
814
- > **Note:** Email endpoints require `AUTH_EMAIL_PROVIDER` to be configured and `enableEmailVerification: true`.
815
-
816
- ### Company Selection Endpoints (when company feature enabled)
817
-
818
- | Endpoint | Method | Auth | Description |
819
- |----------|--------|------|-------------|
820
- | `/auth/select` | POST | No | Select company and branch after login |
821
- | `/auth/switch-company` | POST | JWT | Switch to different company/branch |
822
- | `/auth/get-companies` | POST | JWT | Get user's available companies |
823
- | `/auth/get-company-branches` | POST | JWT | Get branches for a company |
824
-
825
- ### User Management Endpoints
826
-
827
- | Endpoint | Method | Auth | Permission | Description |
828
- |----------|--------|------|------------|-------------|
829
- | `/administration/users/insert` | POST | JWT | `user.create` | Create user |
830
- | `/administration/users/insert-many` | POST | JWT | `user.create` | Bulk create users |
831
- | `/administration/users/get/:id` | POST | JWT | `user.read` | Get user by ID |
832
- | `/administration/users/get-all` | POST | JWT | `user.read` | Get paginated user list |
833
- | `/administration/users/update` | POST | JWT | `user.update` | Update user |
834
- | `/administration/users/update-many` | POST | JWT | `user.update` | Bulk update users |
835
- | `/administration/users/delete` | POST | JWT | `user.delete` | Delete/restore/permanent delete |
836
- | `/administration/users/profile` | POST | JWT | - | Update own profile |
837
- | `/administration/users/:id/verify-email` | POST | JWT | `user.update` | Mark email as verified |
838
- | `/administration/users/:id/verify-phone` | POST | JWT | `user.update` | Mark phone as verified |
839
- | `/administration/users/:id/status` | POST | JWT | `user.update` | Update user active status |
840
-
841
- ### User Profile Extensions Endpoints
842
-
843
- | Endpoint | Method | Auth | Permission | Description |
844
- |----------|--------|------|------------|-------------|
845
- | `/administration/users/:id/profile-extras` | POST | JWT | `user.read` | Get profile with enricher extras |
846
- | `/administration/users/profile-sections` | POST | JWT | `user.read` | Get profile section definitions |
847
- | `/administration/users/:id/profile-section/:sectionId` | POST | JWT | `user.read` | Get specific section data |
848
- | `/administration/users/:id/profile-section/:sectionId/update` | POST | JWT | `user.update` | Update specific section |
849
- | `/administration/users/:id/profile-completion` | POST | JWT | `user.read` | Get profile completion % |
850
-
851
- ### User Permission Endpoints (when company feature enabled)
852
-
853
- | Endpoint | Method | Auth | Permission | Description |
854
- |----------|--------|------|------------|-------------|
855
- | `/administration/permissions/user-company/assign` | POST | JWT | `user.update` | Batch assign/revoke user-company permissions |
856
- | `/administration/permissions/get-user-company` | POST | JWT | `user.read` | Get user's assigned companies |
857
- | `/administration/permissions/user-branch/assign` | POST | JWT | `user.update` | Batch assign/revoke user-branch permissions |
858
- | `/administration/permissions/get-user-branch` | POST | JWT | `user.read` | Get user's assigned branches |
859
-
860
- ### Company Management Endpoints (when company feature enabled)
861
-
862
- | Endpoint | Method | Auth | Permission | Description |
863
- |----------|--------|------|------------|-------------|
864
- | `/administration/company/insert` | POST | JWT | `company.create` | Create company |
865
- | `/administration/company/insert-many` | POST | JWT | `company.create` | Bulk create companies |
866
- | `/administration/company/get/:id` | POST | JWT | `company.read` | Get company by ID |
867
- | `/administration/company/get-all` | POST | JWT | `company.read` | Get paginated company list |
868
- | `/administration/company/update` | POST | JWT | `company.update` | Update company |
869
- | `/administration/company/update-many` | POST | JWT | `company.update` | Bulk update companies |
870
- | `/administration/company/delete` | POST | JWT | `company.delete` | Delete/restore/permanent delete |
871
-
872
- ### Branch Management Endpoints (when company feature enabled)
873
-
874
- | Endpoint | Method | Auth | Permission | Description |
875
- |----------|--------|------|------------|-------------|
876
- | `/administration/branch/insert` | POST | JWT | `branch.create` | Create branch |
877
- | `/administration/branch/insert-many` | POST | JWT | `branch.create` | Bulk create branches |
878
- | `/administration/branch/get/:id` | POST | JWT | `branch.read` | Get branch by ID |
879
- | `/administration/branch/get-all` | POST | JWT | `branch.read` | Get paginated branch list |
880
- | `/administration/branch/update` | POST | JWT | `branch.update` | Update branch |
881
- | `/administration/branch/update-many` | POST | JWT | `branch.update` | Bulk update branches |
882
- | `/administration/branch/delete` | POST | JWT | `branch.delete` | Delete/restore/permanent delete |
883
-
884
- ## Login Flows
885
-
886
- ### Simple Mode (Company Feature Disabled)
596
+ // Validate extra fields before saving
597
+ validateProfileExtras?(userId: string, extras: Record<string, any>, user: ILoggedUserInfo): Promise<void>;
887
598
 
888
- ```
889
- POST /auth/login { email, password }
890
- │
891
- ▼
892
- ┌─────────────────────────┐
893
- │ AuthenticationService │
894
- │ .login() │
895
- └────────────┬────────────┘
896
- │
897
- │ 1. Check pending registration
898
- │ 2. Verify credentials
899
- │ 3. Check email verified (if email enabled)
900
- │ 4. Generate JWT tokens
901
- ▼
902
- { accessToken, refreshToken, user }
903
- ```
599
+ // Return multi-step profile section definitions
600
+ getProfileSections?(): IProfileSection[];
904
601
 
905
- ### Company Mode - Auto-Select (Single Company/Branch)
602
+ // Return data for a specific profile section
603
+ getProfileSectionData?(userId: string, sectionId: string, user: ILoggedUserInfo): Promise<IProfileSectionData>;
906
604
 
907
- ```
908
- POST /auth/login { email, password }
909
- │
910
- ▼
911
- ┌─────────────────────────┐
912
- │ AuthenticationService │
913
- │ .login() │
914
- └────────────┬────────────┘
915
- │
916
- │ 1. Verify credentials
917
- │ 2. Check UserCompanyPermission
918
- │ 3. Found: 1 company, 1 branch → Auto-select
919
- ▼
920
- { accessToken, refreshToken, user, company, branch }
921
- ```
605
+ // Update a specific profile section inside a transaction
606
+ updateProfileSection?(userId: string, sectionId: string, data: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
922
607
 
923
- ### Company Mode - Selection Required (Multiple Options)
608
+ // Handle file upload for a profile section
609
+ handleSectionFileUpload?(userId: string, sectionId: string, field: string, fileInfo: { name: string; url: string; size: number; type: string }, queryRunner: QueryRunner): Promise<IProfileFile>;
924
610
 
925
- ```
926
- POST /auth/login { email, password }
927
- │
928
- ▼
929
- { requiresSelection: true, sessionId, companies: [...] }
930
- │
931
- │ User selects company & branch
932
- ▼
933
- POST /auth/select { sessionId, companyId, branchId }
934
- │
935
- ▼
936
- { accessToken, refreshToken, user, company, branch }
937
- ```
611
+ // Handle file deletion for a profile section
612
+ handleSectionFileDelete?(userId: string, sectionId: string, field: string, fileId: string, queryRunner: QueryRunner): Promise<void>;
938
613
 
939
- ### Login with Email Verification Required
614
+ // Return profile completion percentage (0–100)
615
+ calculateProfileCompletion?(userId: string, user: ILoggedUserInfo): Promise<number>;
940
616
 
941
- ```
942
- POST /auth/login { email, password }
943
- │
944
- ▼
945
- If pending registration OR email not verified:
946
- { requiresEmailVerification: true, email, message }
617
+ // Hook: called after user is created during registration (inside transaction — throw to rollback)
618
+ onUserCreated?(userId: string, additionalFields: Record<string, any> | null, queryRunner: QueryRunner): Promise<void>;
619
+ }
947
620
  ```
948
621
 
949
- ## Email Verification Flow
622
+ **Implementation example (adding IAM roles to user profile):**
950
623
 
951
- ### New User Registration (with email enabled)
624
+ ```typescript
625
+ import { USER_ENRICHER, IUserEnricher } from '@flusys/nestjs-auth';
626
+ import { PermissionService } from '@flusys/nestjs-iam';
952
627
 
953
- ```
954
- ┌─────────────────────────────────────────────────────────────────┐
955
- │ POST /auth/register { email, password, name } │
956
- └─────────────────────────────────────────────────────────────────┘
957
- │
958
- ▼
959
- ┌─────────────────────────────────────────────────────────────────┐
960
- │ 1. Check existing user (must not exist) │
961
- │ 2. Hash password with bcrypt (SALT_ROUNDS = 12) │
962
- │ 3. Generate verification token │
963
- │ 4. Hash token with SHA-256 for storage │
964
- │ 5. Create PendingRegistration record │
965
- │ 6. Send verification email via AUTH_EMAIL_PROVIDER │
966
- └─────────────────────────────────────────────────────────────────┘
967
- │
968
- ▼
969
- { requiresEmailVerification: true, message: "Check your email" }
970
- │
971
- │ User clicks email link
972
- ▼
973
- ┌─────────────────────────────────────────────────────────────────┐
974
- │ POST /auth/verify-email { token } │
975
- └─────────────────────────────────────────────────────────────────┘
976
- │
977
- ▼
978
- ┌─────────────────────────────────────────────────────────────────┐
979
- │ 1. Hash incoming token with SHA-256 │
980
- │ 2. Find PendingRegistration by hashed token │
981
- │ 3. Check expiration │
982
- │ 4. Delete PendingRegistration record │
983
- │ 5. Create User with emailVerified: true │
984
- │ 6. Process company assignment if companyData present │
985
- └─────────────────────────────────────────────────────────────────┘
986
- │
987
- ▼
988
- { accessToken, refreshToken, user, company?, branch? }
989
- ```
628
+ @Injectable()
629
+ export class IamUserEnricher implements IUserEnricher {
630
+ constructor(private readonly permissionService: PermissionService) {}
990
631
 
991
- ### Password Reset Flow
632
+ async getProfileExtras(userId: string, user: ILoggedUserInfo) {
633
+ const roles = await this.permissionService.getRolesForUser(userId);
634
+ const actions = await this.permissionService.getActionsForUser(userId);
635
+ return { roles, actions };
636
+ }
637
+ }
992
638
 
639
+ export const userEnricherProvider = {
640
+ provide: USER_ENRICHER,
641
+ useClass: IamUserEnricher,
642
+ };
993
643
  ```
994
- POST /auth/forgot-password { email }
995
- │
996
- ▼
997
- ┌─────────────────────────────────────────────────────────────────┐
998
- │ 1. Find user by email │
999
- │ 2. Delete existing PASSWORD_RESET tokens for user │
1000
- │ 3. Generate new token, hash with SHA-256 │
1001
- │ 4. Create UserToken record (expires in 1 hour) │
1002
- │ 5. Send reset email via AUTH_EMAIL_PROVIDER │
1003
- │ 6. Return success (same response even if user not found) │
1004
- └─────────────────────────────────────────────────────────────────┘
1005
- │
1006
- ▼
1007
- { success: true, message: "If email exists, reset link sent" }
1008
- │
1009
- │ User clicks email link
1010
- ▼
1011
- POST /auth/reset-password { token, newPassword }
1012
- │
1013
- ▼
1014
- ┌─────────────────────────────────────────────────────────────────┐
1015
- │ 1. Hash incoming token with SHA-256 │
1016
- │ 2. Find UserToken by hashed token + type: PASSWORD_RESET │
1017
- │ 3. Validate not expired, not used │
1018
- │ 4. Update user password │
1019
- │ 5. Mark token as used (usedAt = now) │
1020
- └─────────────────────────────────────────────────────────────────┘
1021
- │
1022
- ▼
1023
- { success: true, message: "Password reset successfully" }
1024
- ```
1025
-
1026
- ## User Management
1027
644
 
1028
- ### Company Permission Assignment Flow
645
+ **Register in `forRootAsync`:**
1029
646
 
1030
- When creating users with `enableCompanyFeature: true`, permissions are automatically assigned:
1031
-
1032
- ```
1033
- User Creation (with companyId/branchId)
1034
- │
1035
- ▼
1036
- ┌─────────────────────────────────────────────────────────────────┐
1037
- │ 1. Insert user record │
1038
- └─────────────────────────────────────────────────────────────────┘
1039
- │
1040
- ▼
1041
- ┌─────────────────────────────────────────────────────────────────┐
1042
- │ 2. Check if company permission exists │
1043
- │ - Lookup UserCompanyPermission for user + company │
1044
- │ - If not exists: create { userId, targetId: companyId, │
1045
- │ permissionType: 'company' } │
1046
- └─────────────────────────────────────────────────────────────────┘
1047
- │
1048
- ▼
1049
- ┌─────────────────────────────────────────────────────────────────┐
1050
- │ 3. Check if branch permission exists │
1051
- │ - Lookup UserCompanyPermission for user + branch │
1052
- │ - If not exists: create { userId, targetId: branchId, │
1053
- │ permissionType: 'branch' } │
1054
- └─────────────────────────────────────────────────────────────────┘
647
+ ```typescript
648
+ AuthModule.forRootAsync({
649
+ // ...
650
+ providers: [userEnricherProvider],
651
+ })
1055
652
  ```
1056
653
 
1057
- ### User Active Status
654
+ All methods in `IUserEnricher` are optional. Implement only the hooks you need.
1058
655
 
1059
- **When company feature is disabled:**
1060
- - Updates `User.isActive` directly
656
+ ---
1061
657
 
1062
- **When company feature is enabled:**
1063
- - Updates `UserCompanyPermission.isActive` for the logged user's company
1064
- - User can be active in Company A but inactive in Company B
658
+ ## Exported Services
1065
659
 
1066
- ### Registration Flows
660
+ These services are exported by `AuthModule` and can be injected into your application:
1067
661
 
1068
- **Simple registration:**
1069
- ```json
1070
- { "name": "John Doe", "email": "john@example.com", "password": "password123" }
1071
- ```
662
+ | Service | Description |
663
+ |---------|-------------|
664
+ | `AuthenticationService` | Core auth logic: login, register, refresh, logout, change password |
665
+ | `UserService` | User CRUD, profile management, enrichment hooks |
666
+ | `CompanyService` | Company CRUD (exported when `enableCompanyFeature: true`) |
667
+ | `BranchService` | Branch CRUD (exported when `enableCompanyFeature: true`) |
668
+ | `UserPermissionService` | User-company-branch assignment (exported when `enableCompanyFeature: true`) |
669
+ | `CompanySelectionSessionService` | Session management for multi-company login flow |
670
+ | `AuthEmailService` | Wraps `AUTH_EMAIL_PROVIDER` for internal email dispatch |
671
+ | `AuthConfigService` | Provides runtime config values (JWT secrets, cookie name, frontend URL) |
672
+ | `AuthDataSourceProvider` | Dynamic TypeORM DataSource resolution per request |
673
+ | `JwtStrategy` | Passport JWT strategy |
674
+ | `JwtAuthGuard` | Guard that validates Bearer token on protected endpoints |
1072
675
 
1073
- **Join existing company:**
1074
- ```json
1075
- {
1076
- "name": "John Doe",
1077
- "email": "john@example.com",
1078
- "password": "password123",
1079
- "companySlug": "acme-corp",
1080
- "branchSlug": "main-branch"
1081
- }
1082
- ```
676
+ **Usage:**
1083
677
 
1084
- **Create new company:**
1085
- ```json
1086
- {
1087
- "name": "John Doe",
1088
- "email": "john@example.com",
1089
- "password": "password123",
1090
- "newCompanyName": "My Company",
1091
- "newCompanyPhone": "+1234567890",
1092
- "newCompanyAddress": "123 Main St"
1093
- }
1094
- ```
678
+ ```typescript
679
+ import { AuthenticationService } from '@flusys/nestjs-auth';
1095
680
 
1096
- **Registration with extension fields (additionalFields):**
1097
- ```json
1098
- {
1099
- "name": "John Doe",
1100
- "email": "john@example.com",
1101
- "password": "password123",
1102
- "additionalFields": {
1103
- "prevCompanyName": "Acme Corporation",
1104
- "prevCompanyLocation": "New York, USA",
1105
- "prevIndustryType": "technology"
681
+ @Injectable()
682
+ export class MyService {
683
+ constructor(
684
+ @Inject(AuthenticationService) private readonly authService: AuthenticationService,
685
+ ) {}
686
+
687
+ async validateToken(token: string) {
688
+ return this.authService.refreshToken(token);
1106
689
  }
1107
690
  }
1108
691
  ```
1109
692
 
1110
- > **Note:** `additionalFields` are passed to the `USER_ENRICHER.onUserCreated` hook within the same database transaction. If the hook throws an error, the entire registration is rolled back, ensuring atomicity.
693
+ > **Note:** Always use `@Inject(ServiceClass)` explicitly. This package is distributed as an esbuild bundle where TypeScript metadata is not preserved.
1111
694
 
1112
- ## No-IAM Mode
695
+ ---
1113
696
 
1114
- When not using `@flusys/nestjs-iam` but controllers use `level: 'permission'` security, set wildcard permissions after login:
697
+ ## Interceptors
1115
698
 
1116
- ```typescript
1117
- import { HybridCache, CACHE_INSTANCE, PERMISSIONS_CACHE_PREFIX } from '@flusys/nestjs-shared';
699
+ | Interceptor | Token | Description |
700
+ |-------------|-------|-------------|
701
+ | `SetToken` | `SetToken` | Writes the refresh token as an HTTP-only cookie on login/register responses |
702
+ | `ClearToken` | `ClearToken` | Clears the refresh token cookie on logout |
1118
703
 
1119
- @Injectable()
1120
- export class AuthenticationService {
1121
- constructor(
1122
- @Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
1123
- ) {}
704
+ Applied automatically by the auth controllers. Export them from `AuthModule` if you need them in custom controllers.
1124
705
 
1125
- async login(dto: LoginDto): Promise<LoginResponse> {
1126
- // ... validate credentials, generate tokens
706
+ ---
1127
707
 
1128
- // When IAM is NOT used, grant all permissions via wildcard
1129
- await this.setWildcardPermissions(user.id);
708
+ ## Security
1130
709
 
1131
- return { accessToken, user };
1132
- }
710
+ ### Token Strategy
1133
711
 
1134
- private async setWildcardPermissions(userId: string): Promise<void> {
1135
- const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:user:${userId}`;
1136
- await this.cache.set(cacheKey, ['*'], 86400000); // 24 hours TTL
1137
- }
1138
- }
1139
- ```
712
+ | Token | Storage | Hashing | Expiry |
713
+ |-------|---------|---------|--------|
714
+ | Access Token (JWT) | Client memory / Authorization header | JWT-signed | Short (e.g. 15m) |
715
+ | Refresh Token | HTTP-only cookie (browser) / request body (mobile) | SHA-256 in DB | Long (e.g. 7d) |
716
+ | Password Reset Token | Email link only | SHA-256 in DB | Short (e.g. 1h) |
717
+ | Email Verification Token | Email link only | SHA-256 in DB | Short (e.g. 24h) |
1140
718
 
1141
- ### With Company Feature Enabled
719
+ ### Password Hashing
1142
720
 
1143
- ```typescript
1144
- private async setWildcardPermissions(
1145
- userId: string,
1146
- companyId?: string,
1147
- branchId?: string,
1148
- ): Promise<void> {
1149
- if (companyId) {
1150
- const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
1151
- await this.cache.set(cacheKey, ['*'], 86400000);
1152
- } else {
1153
- const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:user:${userId}`;
1154
- await this.cache.set(cacheKey, ['*'], 86400000);
1155
- }
1156
- }
1157
- ```
721
+ bcrypt with **12 salt rounds** (OWASP recommended minimum).
1158
722
 
1159
- ### Wildcard Permission Patterns
723
+ ### Rate Limiting
1160
724
 
1161
- | Pattern | Meaning |
1162
- |---------|---------|
1163
- | `*` | Full access to everything |
1164
- | `user.*` | All user permissions |
1165
- | `branch.*` | All branch permissions |
725
+ Built-in via `@nestjs/throttler`:
1166
726
 
1167
- ## JWT Strategy
727
+ | Endpoint | Limit |
728
+ |----------|-------|
729
+ | `POST /auth/login` | 5 requests / 60s |
730
+ | `POST /auth/register` | 3 requests / 60s |
731
+ | `POST /auth/change-password` | 5 requests / 60s |
732
+ | `POST /auth/refresh` | 30 requests / 60s |
1168
733
 
1169
- The `JwtStrategy` is a singleton Passport strategy that validates JWT tokens and extracts user info.
734
+ ### SQL Injection Protection
1170
735
 
1171
- **Token Payload:**
1172
- ```typescript
1173
- interface TokenPayload {
1174
- id: string; // User ID
1175
- email: string; // User email
1176
- type: 'access' | 'refresh'; // Token type
1177
- profilePictureId?: string; // Profile picture file ID
1178
- tenantId?: string; // Tenant ID (multi-tenant)
1179
- iat?: number; // Issued at
1180
- exp?: number; // Expiration
1181
- }
736
+ All database queries use TypeORM parameterized statements. No raw string interpolation.
1182
737
 
1183
- // When company feature enabled
1184
- interface TokenPayloadWithCompany extends TokenPayload {
1185
- companyId: string; // Selected company ID
1186
- branchId?: string; // Selected branch ID (optional)
1187
- companyLogoId?: string | null; // Company logo file ID
1188
- branchLogoId?: string | null; // Branch logo file ID
1189
- }
1190
- ```
738
+ ### Important Notes
1191
739
 
1192
- **Validation Flow:**
1193
- 1. Extract token from `Authorization: Bearer <token>` header
1194
- 2. Verify token signature using JWT secret
1195
- 3. Check token revocation status (if cache available)
1196
- 4. Validate user exists and is active
1197
- 5. Return `ILoggedUserInfo` with user context
740
+ - Never expose `jwtSecret` or `refreshTokenSecret` in version control.
741
+ - Set `frontendUrl` correctly — it is used to construct password-reset and email-verification links sent via email.
742
+ - In production, set `NODE_ENV=production` to enforce secure cookie flags.
1198
743
 
1199
- **Token Revocation:**
1200
- - Uses cache to track revoked tokens
1201
- - Key format: `auth:revoked:{userId}`
1202
- - Stores timestamp of last logout
1203
- - Tokens issued before logout timestamp are rejected
744
+ ---
1204
745
 
1205
- ## Interceptors
746
+ ## Environment Variables
1206
747
 
1207
- ### SetToken Interceptor
748
+ ```env
749
+ # Database
750
+ DB_HOST=localhost
751
+ DB_PORT=5432
752
+ DB_USER=postgres
753
+ DB_PASSWORD=secret
754
+ DB_NAME=myapp
1208
755
 
1209
- Automatically sets refresh token as HTTP-only cookie for browser clients:
756
+ # JWT
757
+ JWT_SECRET=change-me-in-production
758
+ JWT_EXPIRATION=15m
759
+ REFRESH_TOKEN_SECRET=change-me-in-production-2
760
+ REFRESH_TOKEN_EXPIRATION=7d
1210
761
 
1211
- ```typescript
1212
- @UseInterceptors(SetToken)
1213
- async login(@Body() loginDto: LoginDto) { ... }
762
+ # Application
763
+ FRONTEND_URL=http://localhost:3000
764
+ PORT=3001
1214
765
  ```
1215
766
 
1216
- **Behavior:**
1217
- - Detects browser requests via `User-Agent` and `x-client-type` header
1218
- - For browsers: Sets `refreshToken` as HTTP-only cookie, removes from response body
1219
- - For API/Mobile: Returns `refreshToken` in response body
1220
- - Cookie options: `httpOnly: true`, `secure: true` (production), `sameSite: 'lax'`
767
+ ---
768
+
769
+ ## Troubleshooting
1221
770
 
1222
- ### ClearToken Interceptor
771
+ **`Cannot read properties of undefined` on service injection**
1223
772
 
1224
- Clears refresh token cookie on logout:
773
+ You are likely missing `@Inject()` on a constructor parameter. This package is bundled with esbuild and TypeScript metadata is not preserved:
1225
774
 
1226
775
  ```typescript
1227
- @UseInterceptors(ClearToken)
1228
- async logout(@CurrentUser('id') userId: string) { ... }
776
+ // Wrong
777
+ constructor(private readonly authService: AuthenticationService) {}
778
+
779
+ // Correct
780
+ constructor(@Inject(AuthenticationService) private readonly authService: AuthenticationService) {}
1229
781
  ```
1230
782
 
1231
- ## Validators
783
+ ---
1232
784
 
1233
- ### IsStrongPassword
785
+ **Login returns `requiresEmailVerification: true` unexpectedly**
1234
786
 
1235
- Custom password validation decorator:
787
+ The user registered but has not verified their email. Either:
788
+ 1. Provide `AUTH_EMAIL_PROVIDER` so the verification email is actually sent.
789
+ 2. Disable email verification: `enableEmailVerification: false`.
790
+ 3. Use the admin endpoint `POST /administration/users/:id/verify-email` to manually verify.
1236
791
 
1237
- ```typescript
1238
- @IsStrongPassword()
1239
- password!: string;
792
+ ---
1240
793
 
1241
- // With custom options
1242
- @IsStrongPassword({ minLength: 12, requireSpecialChar: false })
1243
- password!: string;
1244
- ```
794
+ **Login returns `requiresSelection: true` unexpectedly**
1245
795
 
1246
- **Default Requirements:**
1247
- - Minimum 8 characters
1248
- - Maximum 128 characters
1249
- - At least one uppercase letter
1250
- - At least one lowercase letter
1251
- - At least one number
1252
- - At least one special character (`!@#$%^&*()_+-=[]{}...`)
796
+ The user belongs to more than one company. Your frontend must handle the `CompanySelectionResponseDto` response and call `POST /auth/company-selection/select` with the `sessionId` and chosen company/branch IDs.
1253
797
 
1254
- **Custom Options:**
1255
- ```typescript
1256
- interface PasswordComplexityOptions {
1257
- minLength?: number; // Default: 8
1258
- maxLength?: number; // Default: 128
1259
- requireUppercase?: boolean; // Default: true
1260
- requireLowercase?: boolean; // Default: true
1261
- requireNumber?: boolean; // Default: true
1262
- requireSpecialChar?: boolean; // Default: true
1263
- }
1264
- ```
798
+ ---
1265
799
 
1266
- ## API Reference
800
+ **`No metadata for entity` error on startup**
1267
801
 
1268
- ### Exports
802
+ Ensure `AuthModule.getEntities()` is called with the correct config when registering `TypeOrmModule`:
1269
803
 
1270
804
  ```typescript
1271
- // Config
1272
- export { AUTH_MODULE_OPTIONS, USER_ENRICHER, BCRYPT_SALT_ROUNDS, DEFAULT_COOKIE_NAME, MAX_COOKIE_BYTES, SESSION_CACHE_PREFIX, TOKEN_REVOKED_PREFIX } from './config';
1273
-
1274
- // Entities
1275
- export { User } from './entities/user.entity';
1276
- export { UserToken, TokenType } from './entities/user-token.entity';
1277
- export { PendingRegistration } from './entities/pending-registration.entity';
1278
- export { Company } from './entities/company.entity';
1279
- export { CompanyBranch } from './entities/company-branch.entity';
1280
- export { UserCompanyPermission, PermissionType } from './entities/user-company-permission.entity';
1281
- export { getEntitiesByConfig, IAuthEntitiesConfig } from './entities';
1282
-
1283
- // Enums
1284
- export { PermissionType } from './enums';
1285
-
1286
- // Interfaces
1287
- export { IAuthEmailProvider, AUTH_EMAIL_PROVIDER } from './interfaces/auth-email-provider.interface';
1288
- export { IUserEnricher, IProfileSection, IProfileSectionData, IProfileFile, IProfileExtras, IProfileRole, IProfileAction, IEnrichedUser } from './interfaces/user-enricher.interface';
1289
- export { AuthModuleOptions, AuthModuleAsyncOptions, AuthOptionsFactory, IAuthModuleConfig } from './interfaces/auth-module-options.interface';
1290
- export { TokenPayload, TokenPayloadWithCompany, LoginResponse, LoginResponseRequiresSelection, LoginResponseRequiresEmailVerification, MeResponse, RegistrationResponse, CompanyWithBranches, BranchForSelection } from './interfaces/authentication.interface';
1291
- export { IUser, IUserMini, IUserWithPassword } from './interfaces/user.interface';
1292
- export { ICompany, ICompanyMini } from './interfaces/company.interface';
1293
- export { ICompanyBranch, ICompanyBranchMini } from './interfaces/company-branch.interface';
1294
-
1295
- // Services
1296
- export { AuthenticationService } from './services/authentication.service';
1297
- export { AuthEmailService } from './services/auth-email.service';
1298
- export { AuthConfigService } from './services/auth-config.service';
1299
- export { AuthDataSourceProvider } from './services/auth-datasource.provider';
1300
- export { UserService } from './services/user.service';
1301
- export { CompanyService } from './services/company.service';
1302
- export { BranchService } from './services/branch.service';
1303
- export { UserPermissionService } from './services/user-permission.service';
1304
- export { CompanySelectionSessionService } from './services/company-selection-session.service';
1305
-
1306
- // Modules
1307
- export { AuthModule } from './modules/auth.module';
1308
-
1309
- // Controllers
1310
- export { AuthenticationController } from './controllers/authentication.controller';
1311
- export { EmailVerificationController } from './controllers/email-verification.controller';
1312
- export { CompanySelectionController } from './controllers/company-selection.controller';
1313
- export { UserController } from './controllers/user.controller';
1314
- export { CompanyController } from './controllers/company.controller';
1315
- export { BranchController } from './controllers/branch.controller';
1316
- export { UserPermissionController } from './controllers/user-permission.controller';
1317
-
1318
- // Strategies
1319
- export { JwtStrategy } from './strategies/jwt.strategy';
1320
-
1321
- // Interceptors
1322
- export { SetToken } from './interceptors/set-token.interceptor';
1323
- export { ClearToken } from './interceptors/clear-token.interceptor';
1324
-
1325
- // Validators
1326
- export { IsStrongPassword, PasswordComplexityOptions } from './validators/password.validator';
1327
-
1328
- // DTOs
1329
- export * from './dtos';
1330
-
1331
- // Swagger Config
1332
- export { authSwaggerConfig } from './docs/auth-swagger.config';
805
+ TypeOrmModule.forRoot({
806
+ entities: [
807
+ ...AuthModule.getEntities({
808
+ enableCompanyFeature: true,
809
+ enableEmailVerification: true,
810
+ }),
811
+ ],
812
+ })
1333
813
  ```
1334
814
 
1335
- ### Module Static Methods
1336
-
1337
- ```typescript
1338
- // Get entities based on feature flags (requires IBootstrapAppConfig)
1339
- AuthModule.getEntities(config: IBootstrapAppConfig): any[]
1340
-
1341
- // Examples:
1342
- AuthModule.getEntities({ enableCompanyFeature: false })
1343
- // Returns: [User, UserToken, PendingRegistration]
1344
-
1345
- AuthModule.getEntities({ enableCompanyFeature: true })
1346
- // Returns: [User, UserToken, PendingRegistration, Company, CompanyBranch, UserCompanyPermission]
1347
-
1348
- AuthModule.getEntities({ enableCompanyFeature: true, enableEmailVerification: false })
1349
- // Returns: [User, UserToken, Company, CompanyBranch, UserCompanyPermission]
815
+ ---
1350
816
 
1351
- // Configure module synchronously
1352
- AuthModule.forRoot(options: AuthModuleOptions): DynamicModule
817
+ **Refresh token cookie not being set**
1353
818
 
1354
- // Configure module asynchronously
1355
- AuthModule.forRootAsync(options: AuthModuleAsyncOptions): DynamicModule
1356
- ```
819
+ Ensure your Express app has `cookie-parser` middleware installed and registered before NestJS:
1357
820
 
1358
- **AuthModuleOptions Interface:**
1359
821
  ```typescript
1360
- interface AuthModuleOptions {
1361
- global?: boolean; // Make module global (default: false)
1362
- includeController?: boolean; // Include default controllers (default: true)
1363
- bootstrapAppConfig?: IBootstrapAppConfig;
1364
- config?: IAuthModuleConfig;
1365
- providers?: Provider[]; // Additional providers (AUTH_EMAIL_PROVIDER, USER_ENRICHER)
1366
- }
1367
-
1368
- interface IAuthModuleConfig {
1369
- defaultDatabaseConfig?: IDatabaseConfig;
1370
- tenantDefaultDatabaseConfig?: IDatabaseConfig;
1371
- tenants?: ITenantDatabaseConfig[];
1372
- jwtSecret?: string;
1373
- jwtExpiration?: string; // Default: '1h'
1374
- refreshTokenSecret?: string;
1375
- refreshTokenExpiration?: string; // Default: '7d'
1376
- refreshTokenCookieName?: string; // Default: 'fsn_refresh_token'
1377
- frontendUrl?: string; // For email links
1378
- }
822
+ import * as cookieParser from 'cookie-parser';
823
+ app.use(cookieParser());
1379
824
  ```
1380
825
 
1381
- ### Swagger Schema Behavior
826
+ ---
827
+
828
+ **Password reset / verification emails not being sent**
1382
829
 
1383
- When `enableCompanyFeature: false`, the following are automatically hidden from Swagger:
830
+ `AUTH_EMAIL_PROVIDER` is optional. If not provided, emails are silently skipped. Implement and register the provider to enable email sending (see [AUTH_EMAIL_PROVIDER](#auth_email_provider)).
1384
831
 
1385
- **Excluded Tags:** Companies, Branches, User Permissions, Company Selection
832
+ ---
1386
833
 
1387
- **Excluded DTO Properties:**
834
+ ## License
1388
835
 
1389
- | DTO | Hidden Fields |
1390
- |-----|---------------|
1391
- | `RegistrationDto` | `companySlug`, `branchSlug`, `newCompanyName`, `newCompanyPhone`, `newCompanyAddress` |
1392
- | `RegistrationResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
1393
- | `LoginResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
1394
- | `MeResponseDto` | `company`, `branch` |
836
+ MIT © FLUSYS
1395
837
 
1396
838
  ---
1397
839
 
1398
- **Last Updated:** 2026-02-25
840
+ > Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.